理解这个机制,是成为React性能优化高手的关键

简介: 理解这个机制,是成为React性能优化高手的关键

本来是准备优先分享两个官方定义的 Hook useMemo,useCallback,不过这两个 hook 本身其实没有太多探讨的空间,他们只是两个记忆函数,本身并没有特殊的、更进一步的含义。


许多人的困惑往往来源于对于它们两个过度解读,认为他们的存在对 React 性能的优化有非常重要的意义。过渡解读导致了对他们的滥用。在我看过的项目中,有个别优秀前端团队里的项目规范里,也错误抬高了他们的作用,把他们用在了每一个组件里。


出现这样问题的根源就在于对 React 的自身机制理解不够精准。因此我决定换一个角度去带大家理解 React 本身的优化机制,从而能够正确的使用 useMemo 与 useCallback。


本文将会从应用层面来为大家分析我们应该怎么做。后续的章节将会从 Fiber 的双缓存策略开始分享底层的优化机制。


01精简节点


首先我们要明确一些前置知识。


React 在内存中维护了一颗 虚拟 DOM 树,这颗树的每一个节点是一个 Fiber,每一个 Fiber 都由 JSX 中的组件解析而来。

type Fiber = {
  // 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent等
  tag: WorkTag,
  // ReactElement里面的key
  key: null | string,
  // ReactElement.type,调用`createElement`的第一个参数
  elementType: any,
  // The resolved function/class/ associated with this fiber.
  // 表示当前代表的节点类型
  type: any,
  // 表示当前FiberNode对应的element组件实例
  stateNode: any,
  // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
  return: Fiber | null,
  // 指向自己的第一个子节点
  child: Fiber | null,
  // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
  sibling: Fiber | null,
  index: number,
  ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,
  // 当前处理过程中的组件props对象
  pendingProps: any,
  // 上一次渲染完成之后的props
  memoizedProps: any,
  // 该Fiber对应的组件产生的Update会存放在这个队列里面
  updateQueue: UpdateQueue<any> | null,
  // 上一次渲染的时候的state
  memoizedState: any,
  // 一个列表,存放这个Fiber依赖的context
  firstContextDependency: ContextDependency<mixed> | null,
  mode: TypeOfMode,
  // Effect
  // 用来记录Side Effect
  effectTag: SideEffectTag,
  // 单链表用来快速查找下一个side effect
  nextEffect: Fiber | null,
  // 子树中第一个side effect
  firstEffect: Fiber | null,
  // 子树中最后一个side effect
  lastEffect: Fiber | null,
  // 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes
  expirationTime: ExpirationTime,
  // 快速确定子树中是否有不在等待的变化
  childExpirationTime: ExpirationTime,
  // fiber的版本池,即记录fiber更新过程,便于恢复
  alternate: Fiber | null,
}

state 的每次变化,都会引发整棵树的前后对比,从而导致许多组件重新执行。这也是 React 性能消耗的主要成本。但是 React 内部采用缓存机制和优秀的 Diff 算法极大的减少了这里的成本,后续我们会详细介绍这两个机制。


这里我要重点介绍的是,在使用中,我们可以通过减小这颗 Fiber tree 的方式来达到性能优化的目的。只要 Fiber tree 足够小,diff 的成本就会非常的低。


例如,我们有一个非常大的巨石项目,当我们路由切换的时候,会直接删掉前一个页面的所有内容,只渲染新页面的内容,那么,虽然随着访问页面的数量越来越多,缓存在全局状态管理器中的数据越来越复杂,但是 Fiber tree 的大小其实并没有变得越来越大,依然维持在一个页面的量级,此时的 diff 压力跟一个小型项目没有什么区别。通过这种手段,我们可以轻松保持一个巨石项目的高性能


落实到具体的页面上,特别是在一些管理系统里,许多开发者喜欢在在列表页中,维护一个内容超级复杂的弹窗组件,弹窗的内容是列表的详情。此时,弹窗内容和列表内容同时存在,从而导致了 Fiber tree 的庞大


从交互上,我们可以将复杂的弹窗内容移植到一个新的详情页,就能极大的缓解 diff 压力。


在某些项目中,一个详情页有几百条表单需要填写。我们可以通过分步骤的方式,把这几百个表单项切分到不同的步骤里,从而让同时渲染出来的表单项大量减少,性能也会有很大的提高


总的来说,只要我们把 Fiber 节点数量控制在一定范围内,React 都能保持一个非常高的性能。因此大多数情况下,我们并不需要做额外的性能优化。


02比较方式


由于大量的 re-render 存在,我们很容易能想到一个优化策略,在 diff 过程中,当我比较之后发现有的节点并没有发生任何变化,那么我们就可以跳过该组件的 re-render,从而提高性能


而要让这个优化想法落地,我们就必须了解内部的比较规则,首先要考虑的第一个问题就是


如何知道一个组件是否发生了变化


一个 React 组件是否发生了变化由三个因素决定


  • props
  • state
  • context


这三个因素中的任何一个发生了变化,组件都会认为自己应该发生变化。state 和 context 都是不可变数据,而且由于是我们主动调用 dispatch 去触发他们发生改变,因此 state 和 context 的变化一般不会对我们造成理解上的困扰


最麻烦的是 props。


React 组件的每次执行,都会传入新的 props 对象,虽然内容可能一样,但是在内存中却已经发生了变化。

function Child(props) {}
// 执行一次传入新的对象
Child({})
// 执行一次传入新的对象
Child({})

与 state 不一样的是,props 并没有缓存在别的地方,因此,一个组件 的 props 哪怕什么都没有变化,比较的结果也是 false

var preProps = {}
var curProps = {}
preProps === curProps // false
var preProps = { name: 'Jake' }
var curProps = { name: 'Jake' }
preProps === curProps // false

也就是说,当一个子组件接收一个函数作为 props,为了保证函数的引用不发生变化,有的人选择使用 useCallback 来缓存函数引用,从而期望子组件不会因为 props 发生了变化而导致子组件重新渲染

function Demo() {
  ...
  const change = useCallback(() => {}, [])
  return (
    <div>
      ...
      <Filter change={change} />
    </div>
  )
}

结合我们刚才说的,这里只使用 useCallback 是做了无用功

preProps = { change: change }
curProps = { change: change }
preProps === curProps // false

那么问题就来了,如果这样子的话,岂不是每个组件的 props 都会发生变化了?


当然不是,React 内部针对 props 有另外一个策略:


如果父组件被判定为没有变化,那么,在判断子组件是否发生变化时,不会比较子组件的 props


源码里少一个判断,却衍生出这样一个精妙的设计


高级!


除此之外,Fiber Tree 的根节点,被判定为始终不会发生变化。


这样,根节点的子组件在比较时,react 就一定会跳过 props 的比较,以此类推。我们就有机会构造一个高性能的更新过程。


回到我们经典的数字递增案例,来分析这个案例

function Child() {
  console.log('我不想重新渲染')
  return (
    <div>我不想重新渲染</div>
  )
}
export default function Demo02() {
  const [count, setCount] = useState(0)
  return (
    <div className="wrapper">
      <div onClick={() => setCount(count + 1)}>{count}</div>
      <Child />
    </div>
  )
}

当我们点击数字的时候,数字递增,父组件 Demo02 被判定为改变,因此,内部的所有子组件都需要比较 props,props 为不可变数据,子组件 Child 的 props 进行了如下比较,结果为 false

{} === {} // false

因此,Child 虽然不想 re-render,但是每次 count 变化都 render 了。


调整的方式非常简单,只需要让父组件的 state 没有发生变化即可,把变化的部分单独封装在另外一个子组件里

function Change() {
  const [count, setCount] = useState(0)
  return (
    <div onClick={() => setCount(count + 1)}>{count}</div>
  )
}
export default function Demo02() {
  return (
    <div className="wrapper">
      <Change />
      <Child />
    </div>
  )
}

这个时候,父组件被判定为没有发生变化,因此子组件就会跳过 props 的比较,从而 Child 判定为没有发生变化。这样我们的目的就达到了。


但是,这里有一个前期条件,那就是我们需要确保 Demo02 的父组件也被判定为没有发生变化,因此,如果你是 React 架构师,顶层结构的设计是你需要关注的重中之重,因为如果顶层出了问题,导致父组件不满足这样的稳定结构,那么后续的子组件都会 re-render。


那么理解这个规则很难吗?其实不难,难就难在,在看这篇文章之前,可能你压根就不知道这个设计啊


如果我们有一个不靠谱的 React 架构师,顶层组件的稳定结构出了问题,那么我们有什么手段,能够低成本的让你能接触到的页面结构保持稳定呢?


答案就是 React.memo


memo 函数会让组件的 props 比较方式发生变化,我们之前都是一直用的 === 全等比较,使用 memo 包裹组件之后,React 内部会改变比较策略,他会遍历 props 的每个属性,如果每个属性都能通过全等比较,那么就判定为 props 没有发生变化


这个遍历过程只会发生在 props 对象的第一层属性,不会更进一步深入


因此,当我们无法确定上层组件是否发生变化时,我们可以在某一个节点使用 memo 来确保从这一层开始建立稳定的高性能模式

function _Child() {
  console.log('我不想重新渲染')
  return (
    <div>我不想重新渲染</div>
  )
}
var Child = memo(_Child)
export default function Demo02() {
  const [count, setCount] = useState(0)
  return (
    <div className="wrapper">
      <div onClick={() => setCount(count + 1)}>{count}</div>
      <Child />
    </div>
  )
}

当我们使用 memo 包裹子组件导致 props 的比较方式发生变化时,useCallback 缓存引用就有用了。这也是 useCallback 的主要作用,他一定要结合 memo 去使用。


当然,我们也可以用一些骚操作来达到同样的目标,利用 useMemo 来缓存组件

export default function Demo02() {
  const [count, setCount] = useState(0)
  const _child = useMemo(() => {
    return <Child />
  }, [])
  return (
    <div className="wrapper">
      <div onClick={() => setCount(count + 1)}>{count}</div>
      {_child}
    </div>
  )
}

当你决定要自己设计比较规则时就可以采用这样的方式。


03总结


这篇文章分享了两个 React 项目性能优化的最重要的手段。我们只要了解了真实的底层机制,就能写出高性能的代码,他们的理解难度并不高。我们只需要在项目中正确的去编写符合他们机制的代码即可。


如果你是 React 项目架构师,那么你一定要吃透这个机制,在顶层架构中,我们会额外添加 Router/Redux 等诸多顶层组件,他们会不会导致高性能结构的崩塌,你一定要非常明确


除此之外,当顶层的父组件不变判定被破坏,我们也不需要每一个组件都用 memo 包裹起来,只需要在合适的节点包裹一个组件即可。因为 memo 的比较本身也会增加程序的执行成本,大量的 memo 反而会导致性能变得更低。


除此之外,我们要明确,组件的 re-render 是内存行为,他是执行了一次 JS 函数,他并不会导致浏览器真的发生渲染行为,因此 re-render 的执行也是非常快速的,大多数情况下的 re-render 都可以接受,不过超大量的 re-render 会导致执行压力变大,所以用大量 memo 减少 re-render 并不一定是一件划算的事情


利用少量的 memo 与 React 本身的缓存机制减少大量的 re-render 才是合理的方案。

相关文章
|
5天前
|
前端开发 JavaScript UED
深入React Hooks与性能优化实践
深入React Hooks与性能优化实践
10 0
|
4月前
|
前端开发 API 开发者
你可能没有关注过的 React 性能优化,帮你突破瓶颈
你可能没有关注过的 React 性能优化,帮你突破瓶颈
|
27天前
|
开发者 搜索推荐 Java
超越传统:JSF自定义标签库如何成为现代Web开发的个性化引擎
【8月更文挑战第31天】JavaServer Faces(JSF)框架支持通过自定义标签库扩展其内置组件,以满足特定业务需求。这涉及创建`.taglib`文件定义标签库及组件,并实现对应的Java类与渲染器。本文介绍如何构建和应用JSF自定义标签库,包括定义标签库、实现标签类与渲染器逻辑,以及在JSF页面中使用这些自定义标签,从而提升代码复用性和可维护性,助力开发更复杂且个性化的Web应用。
48 0
|
28天前
|
前端开发 测试技术 UED
React性能优化的神奇之处:如何用懒加载与代码分割让你的项目一鸣惊人?
【8月更文挑战第31天】在现代Web开发中,性能优化至关重要。本文探讨了React中的懒加载与代码分割技术,通过示例展示了如何在实际项目中应用这些技术。懒加载能够延迟加载组件,提高页面加载速度;代码分割则将应用程序代码分割成多个块,按需加载。两者结合使用,可以显著提升用户体验。遵循合理使用懒加载、编写测试及关注性能等最佳实践,能够更高效地进行性能优化,提升应用程序的整体表现。随着React生态的发展,懒加载与代码分割技术将在未来Web开发中发挥更大作用。
35 0
|
28天前
|
缓存 前端开发 JavaScript
React.memo 与 useMemo 超厉害!深入浅出带你理解记忆化技术,让 React 性能优化更上一层楼!
【8月更文挑战第31天】在React开发中,性能优化至关重要。本文探讨了`React.memo`和`useMemo`两大利器,前者通过避免不必要的组件重渲染提升效率,后者则缓存计算结果,防止重复计算。结合示例代码,文章详细解析了如何运用这两个Hook进行性能优化,并强调了合理选择与谨慎使用的最佳实践,助你轻松掌握高效开发技巧。
38 0
|
2月前
|
缓存 监控 前端开发
react 性能优化方案?
【7月更文挑战第15天】改善React应用性能的关键策略包括:使用生产环境构建减少体积,避免不必要的渲染(如用React.memo或PureComponent),正确设置列表渲染的key,简化组件层级,实施懒加载,避免render中的复杂计算,选择优化过的库,控制重渲染范围,监控性能并合并state更新。这些优化能提升响应速度和用户体验。
33 0
|
2月前
|
前端开发 JavaScript 算法
react【框架原理详解】JSX 的本质、SyntheticEvent 合成事件机制、组件渲染过程、组件更新过程
react【框架原理详解】JSX 的本质、SyntheticEvent 合成事件机制、组件渲染过程、组件更新过程
52 0
|
2月前
|
前端开发
react18【系列实用教程】useContext —— Context 机制实现越层组件传值 (2024最新版)
react18【系列实用教程】useContext —— Context 机制实现越层组件传值 (2024最新版)
36 0
|
2月前
|
前端开发 JavaScript
react18【系列实用教程】useState —— 声明响应式变量(2024最新版)含useState 的异步更新机制,更新的合并,函数传参获取更新值,不同版本异步更新差异,更新对象和数组
react18【系列实用教程】useState —— 声明响应式变量(2024最新版)含useState 的异步更新机制,更新的合并,函数传参获取更新值,不同版本异步更新差异,更新对象和数组
111 0
|
3月前
|
人工智能 监控 前端开发
基于ReAct机制的AI Agent
当前,在各个大厂纷纷卷LLM的情况下,各自都借助自己的LLM推出了自己的AI Agent,比如字节的Coze,百度的千帆等,还有开源的Dify。你是否想知道其中的原理?是否想过自己如何实现一套AI Agent?当然,借助LangChain就可以。