React 性能优化终章,成为顶尖高手的最后一步

简介: React 性能优化终章,成为顶尖高手的最后一步

在前面的章节中,我们学习了 context 的使用方式,基于它我们可以搞一个自己的状态管理库。不过,他存在性能上的问题,以致于虽然从功能的实现上来说,他非常不错,但是从性能上来说,context 的表现非常糟糕,虽然很少有 React 学习者关注到这个问题,但是如果你关注项目的整体架构,并且想要成为顶尖高手的话,这是你必须掌握的最后一步。


接下来我们会用案例来探讨 context 存在什么样的性能问题,并思考如何设计一个方案来替代 context,解决它的性能问题


一、context 存在啥问题


我们需要通过一个实践案例来分析 context 存在的性能问题。我计划把几个不同的 counter 状态分散放到不同的子组件中去。项目结构如图

+ App
  - index.tsx
  - Provider.tsx
  - Counter01.tsx
  - Counter02.tsx
  - Counter03.tsx
  - Reset.tsx

在入口文件中,使用 Provider 把所有的子组件包裹起来

import Provider from './Provider';
import Counter01 from './Counter01';
import Counter02 from './Counter02';
import Counter03 from './Counter03';
import Reset from './Reset';
/**
 * @description 性能有问题,子组件每次都会rerender
 * @returns 
 */
export default function App() {
  return (
    <Provider>
      <Counter01 />
      <Counter02 />
      <Counter03 />
      <Reset />
    </Provider>    
  )
}

在 Provider 中,我们创建好 context,并在 state 中定义好数据,并通过 value 向子组件传递

import {createContext, Dispatch, SetStateAction, useState} from 'react'
interface Props {
  children: any
}
const initialState = {
  counter01: 0,
  counter02: 0,
  counter03: 0
}
type State = typeof initialState
interface Value extends State {
  setCounter01: Dispatch<any>,
  setCounter02: Dispatch<any>,
  setCounter03: Dispatch<any>
}
export const context = createContext<Value>(initialState as Value)
export default function Provider(props: Props) {
  const [state, setState] = useState(initialState)
  const value = {
    ...state,
    setCounter01: (value: number) => setState({...state, counter01: value}),
    setCounter02: (value: number) => setState({...state, counter02: value}),
    setCounter03: (value: number) => setState({...state, counter03: value})
  }
  return (
    <context.Provider value={value}>
      {props.children}
    </context.Provider>
  )
}

每个子组件里,都会显示一个 counter,并带有一个按钮点击能递增 counter,为了方便查看该子组件是否被 re-render,我们会在内部逻辑中执行 console.log 来观察

import { useContext } from 'react';
import {context} from './Provider'
export default function Counter01() {
  const {counter01, setCounter01} = useContext(context)
  console.log('counter01: ', counter01)
  function clickHandle() {
    setCounter01(counter01 + 1)
  }
  return (
    <button onClick={clickHandle}>
      counter01: {counter01}
    </button>
  )
}

除此之外,为了验证 memo 的效果,我们还使用 memo 将一个子组件包裹起来

import { useContext, memo } from 'react';
import {context} from './Provider'
function Counter03() {
  const {counter03, setCounter03} = useContext(context)
  console.log('counter03: ', counter03)
  function clickHandle() {
    setCounter03(counter03 + 1)
  }
  return (
    <button onClick={clickHandle}>
      counter03: {counter03}
    </button>
  )
}
export default memo(Counter03)

Reset 组件中只会重置对应的数据为初始状态

import { useContext } from 'react';
import {context} from './Provider'
export default function Reset() {
  const {setCounter01, setCounter02} = useContext(context)
  console.log('reset');
  function clickHandle() {
    setCounter01(0);
    // setCounter02(1);
  }
  return (
    <div>
      <button onClick={clickHandle}>
        Reset01 02 to 0
      </button>
    </div>
  )
}

OK,全部代码大概如此。运行,测试之后,我们发现此时存在严重的 re-render 现象:当我们修改任何一个状态时,所有的子组件都会 re-render,即使这个组件跟这个状态毫无关系。就算你使用 memo 将子组件包裹起来,该子组件依然会 re-render。因此,当你基于 context 开发顶层状态管理器时,你的 React 项目的性能,将会很差。


梳理一下,具体的糟糕表现为:


  • 1、任何状态的变化,所有子组件都会 re-render
  • 2、子组件包裹 memo 无效
  • 3、连续点击 reset 按钮,即使状态没有发生变化,所有子组件也会 re-render


为什么会出现这个问题呢?


我们前面已经分析过,React 组件的 re-render 机制,需要同时保证 state、props、context 都不变,组件才不会 re-render


我们观察一下 Provider 的写法

export default function Provider(props: Props) {
  const [state, setState] = useState(initialState)
  const value = {
    ...state,
    setCounter01: (value: number) => setState({...state, counter01: value}),
    setCounter02: (value: number) => setState({...state, counter02: value}),
    setCounter03: (value: number) => setState({...state, counter03: value})
  }
  return (
    <context.Provider value={value}>
      {props.children}
    </context.Provider>
  )
}

在 context 发生变化时,value 总会被重新声明,context.Provider 的 props.value 总是会发生变化,那么他的子组件的稳定结构从顶层就被破坏了,因此当 state 发生变化时,被他包裹的所有子组件都会 re-render。


二、context 的替代方案


在思考 context 的替代方案之前,我们先总结一下 context 的能力。


1、支持全局共享状态

2、支持跨组件传递


那么,我们如何基于 React 现有的机制,做到和 context 一样的事情呢?要单独想到比较困难,但是答案却非常简单。具体的思路是,我们可以利用发布订阅模式,收集每个组件内部的 setState,把共享状态的 satate 收集到一起,然后利用他们各自的 setState 去触发数据的更新即可。这样,我们就可以实现上面的两个要求了。


创建一个 store.ts 文件来完成我们的构想。


首先创建一个对象用来存储所有的数据,并约定好数据的格式

interface StoreItem {
  value: any,
  dispatch: Set<any>
}
interface Store {
  [key: string]: StoreItem
}
const store: Store = {}

理解这个数据格式,是整个功能实现的关键。不同的数据会对应不同的 key 值,相同的数据会对应不同的 setState,我们在 store 中用对应的格式把这个关系存储起来。


另外我再单独定义一个对象,去存储每一个状态的初始化状态

interface KeyMap {
  [key: string]: boolean
}
const isInitStore: KeyMap = {}

修改数据,本质上是执行 setState,因此,我们需要先定义好一个 set 方法用于触发存储在 dispatch 中的所有 setState 执行,该方法只能在 store 模块内部被调用。

function _setValue(key: string, value: any) {
  store[key].value = value
  store[key].dispatch.forEach((cb: any) => {
    cb(value)
  })
}

我们还需要定义一个 useSubscribe 用于在子组件内部订阅状态。该方法用于收集每个组件的 setState,并返回当前组件对应的状态,和修改该状态的方法

export function useSubscribe(key: string, value?: any) {
  const [state, setState] = useState(value || null)
  // 如果没有被初始化,则初始化一次
  if (!isInitStore[key]) {
    store[key] = { value: value, dispatch: new Set() }
    isInitStore[key] = true
  }
  if (store[key].dispatch.has(setState) === false) {
    store[key].dispatch.add(setState)
  }
  return [state, (_value: any) => _setValue(key, _value)]
}

有的时候我们还需要单独调用某个方法去修改全局的状态,因此,我们还需要对外抛出一个 useDispatch 来完成这个需求

export function useDispatch(key: string) {
  return (value: any) => _setValue(key, value)
}

OK,简单的代码,我们的这个功能就设计好了。我们在子组件中使用他们一下试试看。在子组件中使用时,只需要使用 useSubscribe 订阅一下即可。该方法返回了状态值,和修改状态值的 set 方法。

import { useSubscribe } from './store';
export default function Counter01() {
  const [counter, setCounter] = useSubscribe('counter01')
  console.log('counter01: ', counter)
  function clickHandle() {
    setCounter(counter + 1)
  }
  return (
    <button onClick={clickHandle}>
      counter01: {counter}
    </button>
  )
}

这里传入的字符串非常关键,如果你在不同的组件中共享同一个数据,那么他们传入的 key 值需要保持一致才能做到共享。例如我们分别定义下面两个组件,他们能共享同一个状态

import { useSubscribe } from './store';
function Counter03() {
  const [counter, setCounter] = useSubscribe('counter04')
  console.log('counter03: ', counter)
  function clickHandle() {
    setCounter(counter + 1)
  }
  return (
    <button onClick={clickHandle}>
      counter03: {counter}
    </button>
  )
}
export default Counter03
import {useSubscribe} from './store'
export default function Counter04() {
  const [counter, setCounter] = useSubscribe('counter04')
  console.log('counter04: ', counter)
  function clickHandle() {
    setCounter(counter + 1)
  }
  return (
    <button onClick={clickHandle}>
      counter04: {counter}
    </button>
  )
}

如果我们要单独在别的组件中修改全局状态,则可以利用 useDispatch

import { useDispatch } from './store';
export default function Reset() {
  const setCounter01 = useDispatch('counter01')
  const setCounter02 = useDispatch('counter02')
  const setCounter03 = useDispatch('counter04')
  console.log('reset');
  function clickHandle() {
    setCounter01(0);
    setCounter02(0);
  }
  function clickHandle03() {
    setCounter03(0)
  }
  return (
    <div>
      <button onClick={clickHandle}>
        Reset01 02 to 0
      </button>
      <button onClick={clickHandle03}>
        Reset03
      </button>
    </div>
  )
}

程序运行起来之后,测试一下。


发现我们不仅实现了全局状态共享,也实现了数据跨组件传递。也解决了 context 引发不相干子组件刷新的问题。甚至组组件连 memo 的优化手段都不需要用,依然能够保持最低代价的 re-render。也就是说,这种方案完美解决了 context 的性能弊病,成为了一个高性能方案。因此,基于你的需求稍微扩展一下,他就能够成为一个强大的状态管理库运用于你的真实项目中。


在前面的篇幅中,我有强调过 React 对 JavaScript 的弱侵入性是他的一大优势。在这个方案里,已经展现出来这一优势的巨大作用。我们有机会利用各种 JavaScript 的解决方案运用到我们的项目中,扩展 React 的项目边界。


三、总结


我们这个方案基于闭包,利用发布订阅模式,在子组件中订阅组件对应的 setState,并在执行时统一触发所有相同状态的 set 方法。如果对我标黑的几个基础知识掌握得比较好的话,对这个方案理解起来会比较容易。否则可能会面临比较大的理解成本。不过也没有关系,加入 React 知命境付费群,可以在群里跟群友进一步探讨该方案,我也会在群里直播讲解该方案


除了我们自己利用发布订阅模式来解决该问题之外,React 官方文档也提供了一个 hook 来达到类似的效果:useSyncExternalStore,因为直接学习它有不少理解成本,因此我们铺垫了本文的方案,后续会专门写一篇文章来学习它,包括我们熟知的状态管理方案 zustand 也是基于这个 hook 来实现。

相关文章
|
1月前
|
存储 前端开发 JavaScript
深入理解React Fiber架构及其性能优化
【10月更文挑战第5天】深入理解React Fiber架构及其性能优化
87 1
|
6月前
|
前端开发 API 开发者
你可能没有关注过的 React 性能优化,帮你突破瓶颈
你可能没有关注过的 React 性能优化,帮你突破瓶颈
|
2月前
|
前端开发 JavaScript UED
深入React Hooks与性能优化实践
深入React Hooks与性能优化实践
48 0
|
3月前
|
开发者 搜索推荐 Java
超越传统:JSF自定义标签库如何成为现代Web开发的个性化引擎
【8月更文挑战第31天】JavaServer Faces(JSF)框架支持通过自定义标签库扩展其内置组件,以满足特定业务需求。这涉及创建`.taglib`文件定义标签库及组件,并实现对应的Java类与渲染器。本文介绍如何构建和应用JSF自定义标签库,包括定义标签库、实现标签类与渲染器逻辑,以及在JSF页面中使用这些自定义标签,从而提升代码复用性和可维护性,助力开发更复杂且个性化的Web应用。
73 0
|
3月前
|
前端开发 测试技术 UED
React性能优化的神奇之处:如何用懒加载与代码分割让你的项目一鸣惊人?
【8月更文挑战第31天】在现代Web开发中,性能优化至关重要。本文探讨了React中的懒加载与代码分割技术,通过示例展示了如何在实际项目中应用这些技术。懒加载能够延迟加载组件,提高页面加载速度;代码分割则将应用程序代码分割成多个块,按需加载。两者结合使用,可以显著提升用户体验。遵循合理使用懒加载、编写测试及关注性能等最佳实践,能够更高效地进行性能优化,提升应用程序的整体表现。随着React生态的发展,懒加载与代码分割技术将在未来Web开发中发挥更大作用。
52 0
|
3月前
|
缓存 前端开发 JavaScript
React.memo 与 useMemo 超厉害!深入浅出带你理解记忆化技术,让 React 性能优化更上一层楼!
【8月更文挑战第31天】在React开发中,性能优化至关重要。本文探讨了`React.memo`和`useMemo`两大利器,前者通过避免不必要的组件重渲染提升效率,后者则缓存计算结果,防止重复计算。结合示例代码,文章详细解析了如何运用这两个Hook进行性能优化,并强调了合理选择与谨慎使用的最佳实践,助你轻松掌握高效开发技巧。
94 0
|
4月前
|
缓存 监控 前端开发
react 性能优化方案?
【7月更文挑战第15天】改善React应用性能的关键策略包括:使用生产环境构建减少体积,避免不必要的渲染(如用React.memo或PureComponent),正确设置列表渲染的key,简化组件层级,实施懒加载,避免render中的复杂计算,选择优化过的库,控制重渲染范围,监控性能并合并state更新。这些优化能提升响应速度和用户体验。
54 0
|
6月前
|
前端开发 数据可视化 UED
React的代码分割:使用React.lazy和Suspense进行性能优化
【4月更文挑战第25天】使用React的`React.lazy`和`Suspense`进行代码分割可优化性能,按需加载组件以提升应用启动速度和用户体验。`React.lazy`接收返回Promise的组件动态导入,而`Suspense`提供加载指示器,保证加载过程中的用户体验。适用于大型组件或路由应用,但需注意服务器配置、避免过度拆分和确保关键代码即时加载。合理运用能显著改善应用性能。
|
缓存 前端开发 JavaScript
react中的性能优化方案有哪些
react中的性能优化方案有哪些
72 0
|
6月前
|
前端开发 JavaScript
React 性能优化新招,useTransition
React 性能优化新招,useTransition
下一篇
无影云桌面