React Memo不是你优化的第一选择(二)

本文涉及的产品
表格存储 Tablestore,50G 2个月
简介: React Memo不是你优化的第一选择(二)

解法 2:内容提升

当一部分state在高开销组件树的上层代码中使用时下放State就无法奏效了。举个例子,如果我们将color放到父元素div中。

export default function App() {
+  let [color, setColor] = useState('red');
  return (
+    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p>Hello, 789!</p>
      <ExpensiveComponent />
    </div>
  );
}

我们将App组件分割为两个子组件。依赖color的代码就和color state变量一起放入ColorPicker组件里。 不关心color的部分就依然放在App组件中,然后以JSX内容的形式传递给ColorPicker,也被称为children属性。 当color变化时,ColorPicker会重新渲染。但是它仍然保存着上一次从App中拿到的相同的children属性,所以React并不会访问那棵子树。 因此,ExpensiveComponent不会重新渲染。

代码改造如下:

function App() {
  return (
    <ColorPicker>
+      <p>Hello, 789!</p>
+      <ExpensiveComponent />
    </ColorPicker>
  )
}
function ColorPicker({ children }) {
  const [color, setColor] = React.useState('red')
  return (
    <div style={{ color }}>
      <input
        value={color}
        onChange={(event) => setColor(event.target.value)}
      />
+      {children}
    </div>
  )
}

如果子组件始终是完全相同的引用React可以直接跳过渲染过程。即使颜色发生变化,ExpensiveComponent也不会随之重新渲染。

上面两种解法,都是利用组件组合,从而避免重复渲染。下面,我们采用React.memo的语法,看看会发生啥。

解法 3:React.memo

另外的方案就是将所有内容都从同一个组件内部进行渲染,但在ExpensiveComponent组件周围包裹一个React.memo

function App() {
  const [color, setColor] = useState('red')
  return (
    <div style={{ color }}>
      <input
        value={color}
        onChange={(event) => setColor(event.target.value)}
      />
      <p>Hello, 789!</p>
+     <ExpensiveTree />
    </div>
  )
}
function ExpensiveComponent() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 手动模拟,耗时任务 -- 此处会卡顿100ms
  }
  // 打印被渲染的次数
  console.log('我被渲染了');
  return <p>耗时渲染</p>;
}
+ const ExpensiveTree = React.memo(ExpensiveComponent)

如果我们使用React.memo包装一个组件,如果其props没有发生变化,React将跳过渲染该组件(以及其子组件)。这当然可以实现与改变组件组合相同的结果,但在将来容易出现问题

当一个组件被Memo处理后,React将使用Object.is比较每个prop。如果它们没有发生变化,就可以跳过重新渲染。

上面利用React.Memo处理的情况就满足要求,因为我们的组件实际上没有props。它也适用于将原始值作为props,但对于函数对象数组等情况,效果就不那么好了。

大家是否还记得,针对JS来说,函数对象数组等非基本数据类型,它们是存在堆中的,也就是在引用它们的时候,我们只是引用了它存在堆中的地址(指针)。关于这个知识点,可以看我们之前的写的JS篇之数据类型那些事儿

让我们对ExpensiveComponent进行一个看似无害的更改 - 添加一个style prop

function ExpensiveComponent({ style }) {
  return <div style={style}>耗时任务</div>
}
const ExpensiveTree = React.memo(ExpensiveComponent)

通常情况下,组件随着时间的推移会逐渐演化,props会被添加进来。问题在于,ExpensiveTree组件的使用者并不一定知道它是否被Memo处理。毕竟,这是ExpensiveComponent的使用细节。

如果现在在渲染ExpensiveTree组件时添加一个内联样式prop

<ExpensiveTree style={{ backgroundColor: 'blue' }} />

我们无意中破坏了记忆化,因为style prop每次渲染时都会是一个新的对象。对于React来说,看起来props已经发生变化,因此它无法跳过渲染。

好的,当然,我们可以通过将style prop包装在React.useMemo中来解决这个问题:

function App() {
+  const memoizedStyle = React.useMemo(
+    () => ({ backgroundColor: 'blue' }),
+    []
+  )
  return <ExpensiveTree style={memoizedStyle} />
}

在我们这个简单的情况下是可行的,但想象一下,如果我们有更多需要记忆化的props,我们的代码将会变得更加难以理解,而且不能保证使用者会对数据进行Memo处理

而当style本身作为一个prop传递给渲染ExpensiveTree的组件时,情况会变得更加复杂:

function App({ style }) {
+  const memoizedStyle = React.useMemo(() => style, [style])
  return <ExpensiveTree style={memoizedStyle} />
}

这种记忆化实际上没有实现任何效果。我们无法确定style是否作为内联对象传递给App,因此在这里进行记忆化是没有意义的。我们需要在App调用处创建一个稳定的引用


3. children

另一个需要注意的地方是,如果被Memo处理过的组件接受children,它们的行为可能不如我们期望的那样:

function App() {
  return (
    <ExpensiveTree>
+      <p>Hello, 789!</p>
    </ExpensiveTree>
  )
}
+ function ExpensiveComponent({ children }) {
  return (
    <div>
      耗时任务
+      {children}
    </div>
  )
}
const ExpensiveTree = React.memo(ExpensiveComponent)

上面的代码看起来人畜无害,但是它却在破坏我们组件稳定上包藏祸心

为什么会破坏呢?表面上,我总是传递相同的、稳定的<p>标签作为children。实际上并不是。JSX只是React.createElement的语法糖,它会在每次渲染时创建一个新的对象。因此,尽管对我们来说<p>标签看起来是相同的,但它们不是相同的引用

当然,我们可以将传递给记忆化组件的children包装在useMemo中,这无疑让我们悉心呵护的组件陷入"人民战争"的汪洋大海。 因为,你永远不知道,下个弄潮儿不知道在什么地方,什么时机没做Memo处理。如果这样的话,兜兜转转我们又回到了原点。

下面的代码大家肯定熟悉。只传递一个空对象或数组作为记忆化组件的prop的回退值。如果这样,我们总不能对[]进行记忆处理。如果这么做也没错,这无疑让我们的代码变成老太婆的裹脚布又臭又长

<ExpensiveTree someProp={someStableArray ?? []} />

4. 替代方案

因此,使用React.memo有一些潜在问题,但有时,似乎我们无法避免对一个组件进行记忆化处理。那是否又一个折中的方案来解决这种问题呢?有!!

其实,我们大部分的组件很少需要进行React性能优化。凡事就怕一个万一。

假设,我们有一个页面,其中包含5个大表格和一个摘要栏。当一个表格发生变化时,所有内容都重新渲染。这导致页面加载速度很慢。

代码结构如下,出于简洁起见,使用了两个表格而不是五个:

function App() {
  const [state, setState] = React.useState({
    table1Data: [],
    table2Data: [],
  })
  return (
    <div>
      <Table1 data={state.table1Data} />
      <Table2 data={state.table2Data} />
      <SummaryBar />
    </div>
  )
}

state保存了两个表格的数据,而SummaryBar需要访问所有这些数据。我们无法将状态下放到表格中,也无法以不同的方式组合这些组件。似乎对组件进行memo处理是我们唯一的选择。

其实在twitter上已经有人给出了解决方案。

image.png

解决方案就是:

  • 将每个表格包裹在React.memo中。
  • 将传递的函数包裹在useCallback中。

但是,我们再另辟蹊径,用其他方式解决这个问题。


不要开始渲染

还记得我之前说过一旦渲染开始,我们就没有办法停止它吗?这仍然是正确的,但如果我们从一开始就阻止渲染呢... 🤔

如果状态不位于应用程序的顶部,我们就不需要在它发生变化时重新渲染整个树。

但它可以放在哪里呢?我们已经确定无法将其下移 - 那么就将其放在一边 - 放在React触及不到的地方。

这正是大多数状态管理解决方案所做的。它们将状态存储在React之外,并有针对性地触发需要更改的组件树部分的重新渲染。像React Query /zustand/Recoil都是这么做的。

因此,是的,我提出的替代解决方案是引入一个有效的状态管理器。下面我们使用zustand来演示。(当然,也可以换成你熟悉的状态管理库)

这里多说一点,之前在React-全局状态管理的群魔乱舞我们讲过各个库的适用场景。

const useTableStore = create((set) => ({
  table1Data: [],
  table2Data: [],
  actions: {...}
}))
export const useTable1Data = () =>
  useTableStore((state) => state.table1Data)
export const useTable2Data = () =>
  useTableStore((state) => state.table2Data)
export const useSummaryData = () =>
  useTableStore((state) =>
    calculateSummary(state.table1Data, state.table2Data)
  )

现在,每个组件都可以内部订阅其感兴趣的状态,避免了任何自顶向下的重新渲染。如果table2Data更新,Table1将不会重新渲染。这与对表格进行记忆化一样有效,但不会受到添加新props可能对性能产生负面影响的问题。


5. 问题的根源

无论是使用组件组合的方式还是使用React.memo亦或者利用状态管理器都不是最佳选择。

  • 进行记忆化会使我们的代码难以阅读,而且很容易出错 (最差)
  • 使用外部状态管理器会稍微好一些,但是增加了我们项目的学习负担 (稍好)
  • 组件组合似乎可以完美解决,但是有些组件的改造可不是像上面Demo一样,做一次层级的改变,而是需要大刀阔斧。无疑增加了心智负担。(相比其他的解决方案,还是优先推荐)

出现这个问题的真正根源还是。非基本数据类型的特性导致的。就像上面讲到的那样,函数/对象/数组这种数据对比。所以真正的解决之道是改变游戏规则。RecordsTuples,它们可以帮助我们处理数组和对象,但不适用于函数。

React团队也曾暗示他们正在开发一个名为React Forget的编译器,据说将自动为我们进行记忆化。有了这个工具,我们可以获得React.memo的性能优化,同时减少错误的发生机会。


后记

分享是一种态度

参考链接:

  1. memoization
  2. proposal-record-tuple
  3. React performance optimizations
  4. records-and-tuples-for-react
  5. before-you-memo

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

相关实践学习
消息队列+Serverless+Tablestore:实现高弹性的电商订单系统
基于消息队列以及函数计算,快速部署一个高弹性的商品订单系统,能够应对抢购场景下的高并发情况。
阿里云表格存储使用教程
表格存储(Table Store)是构建在阿里云飞天分布式系统之上的分布式NoSQL数据存储服务,根据99.99%的高可用以及11个9的数据可靠性的标准设计。表格存储通过数据分片和负载均衡技术,实现数据规模与访问并发上的无缝扩展,提供海量结构化数据的存储和实时访问。 产品详情:https://www.aliyun.com/product/ots
相关文章
|
7月前
|
前端开发 JavaScript UED
使用React Hooks优化前端应用性能
本文将深入探讨如何使用React Hooks来优化前端应用的性能,重点介绍Hooks在状态管理、副作用处理和组件逻辑复用方面的应用。通过本文的指导,读者将了解到如何利用React Hooks提升前端应用的响应速度和用户体验。
|
2月前
|
前端开发
React Memo
10月更文挑战第11天
33 6
|
2月前
|
前端开发 JavaScript 算法
React 渲染优化策略
【10月更文挑战第6天】React 是一个高效的 JavaScript 库,用于构建用户界面。本文从基础概念出发,深入探讨了 React 渲染优化的常见问题及解决方法,包括不必要的渲染、大量子组件的渲染、高频事件处理和大量列表渲染等问题,并提供了代码示例,帮助开发者提升应用性能。
55 6
|
2月前
|
JSON 前端开发 JavaScript
【简单粗暴】如何使用 React 优化 AG 网格性能
【简单粗暴】如何使用 React 优化 AG 网格性能
31 3
|
3月前
|
缓存 前端开发
React中函数式Hooks之memo、useCallback的使用以及useMemo、useCallback的区别
React中的`memo`是高阶组件,类似于类组件的`PureComponent`,用于避免不必要的渲染。`useCallback` Hook 用于缓存函数,避免在每次渲染时都创建新的函数实例。`memo`可以接收一个比较函数作为第二个参数,以确定是否需要重新渲染组件。`useMemo`用于缓存计算结果,避免重复计算。两者都可以用来优化性能,但适用场景不同:`memo`用于组件,`useMemo`和`useCallback`用于值和函数的缓存。
95 1
|
7月前
|
监控 前端开发 API
如何优化React性能?
【4月更文挑战第9天】提升React应用性能的关键策略包括:使用React.memo和PureComponent防止不必要的重渲染;实施代码分割减少初始加载时间;借助React Profiler定位性能问题;优化state和props以减小大小和复杂性;设置列表项的key属性;避免内联函数和对象;自定义shouldComponentUpdate或React.memo比较函数;使用虚拟化技术渲染大量列表;通过Context API共享数据;以及利用服务端渲染加速首屏加载。不断监控和调整是优化的核心。
61 9
|
4月前
|
前端开发 JavaScript 大数据
React与Web Workers:开启前端多线程时代的钥匙——深入探索计算密集型任务的优化策略与最佳实践
【8月更文挑战第31天】随着Web应用复杂性的提升,单线程JavaScript已难以胜任高计算量任务。Web Workers通过多线程编程解决了这一问题,使耗时任务独立运行而不阻塞主线程。结合React的组件化与虚拟DOM优势,可将大数据处理等任务交由Web Workers完成,确保UI流畅。最佳实践包括定义清晰接口、加强错误处理及合理评估任务特性。这一结合不仅提升了用户体验,更为前端开发带来多线程时代的全新可能。
82 1
|
4月前
|
前端开发 UED 开发者
React组件优化全攻略:深度解析让你的前端应用飞速运行的秘诀——从PureComponent到React.memo的彻底性能比较
【8月更文挑战第31天】在构建现代Web应用时,性能是提升用户体验的关键因素。React作为主流前端库,其组件优化尤为重要。本文深入探讨了React组件优化策略,包括使用`PureComponent`、`React.memo`及避免不必要的渲染等方法,帮助开发者显著提升应用性能。通过实践案例对比优化前后效果,不仅提高了页面渲染速度,还增强了用户体验。优化React组件是每个开发者必须关注的重点。
69 0
|
4月前
|
前端开发 UED 开发者
React.lazy()与Suspense:实现按需加载的动态组件——深入理解代码分割、提升首屏速度和优化用户体验的关键技术
【8月更文挑战第31天】在现代Web应用中,性能优化至关重要,特别是减少首屏加载时间和提升用户交互体验。React.lazy()和Suspense组件提供了一种优雅的解决方案,允许按需加载组件,仅在需要渲染时加载相应代码块,从而加快页面展示速度。Suspense组件在组件加载期间显示备选内容,确保了平滑的加载过渡。
149 0
|
4月前
|
缓存 前端开发 JavaScript
深入探索优化React应用程序的策略
【8月更文挑战第20天】
44 0