[译] 用 React Hooks 和调试工具提升应用性能[通俗易懂]

idea (61) 2023-03-25 08:03

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说[译] 用 React Hooks 和调试工具提升应用性能[通俗易懂],希望能够帮助你!!!。

  • 原文地址:Increase your App’s performance with React hooks and the React Dev Tools
  • 原文作者:Koen Poelhekke
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:Baddyo
  • 校对者:wuyanan,Jerry-FD
[译] 用 React Hooks 和调试工具提升应用性能[通俗易懂]_https://bianchenghao6.com/blog_idea_第1张

在构建 React 应用时,你会发现随着嵌套组件增多,用户界面的某些部分开始变得缓慢迟滞。这是因为,被改变 state 的元素在组件树中的层级越高,浏览器需要重绘的组件越多。

本文将告诉你如何通过备忘(memoization)技术避免不必要的重绘,让你的 React 应用快如闪电。⚡


在 CLEVER°FRANKE 的一个客户端项目中,我做过一个过滤器组件,该组件包含一个展示步数的直方图。

[译] 用 React Hooks 和调试工具提升应用性能[通俗易懂]_https://bianchenghao6.com/blog_idea_第2张

我发现每当拖拽过滤器的操纵杆,动画帧率就会骤降,导致组件失去效用。故此我决定一探究竟。

抽丝剥茧

只有明白用户拖拽操纵杆时的内部运作原理,才能确定从何处下手调试。React 使用虚拟 DOM 来代表 DOM 中真实的元素。每当用户操作界面元素,应用的 state 都会改变。React 会遍历所有受 state 改变影响的组件,计算生成新的虚拟 DOM。React 将新旧版本的虚拟 DOM 进行比较,若发现二者有差异,就将对应的变化更新到真实 DOM 上。该过程叫做 reconciliation。

操纵 DOM 元素可是一个非常耗费资源的任务。同样,遍历所有受影响组件的 render 函数也很耗时,render 函数中的计算量很大时尤其如此。因此我们要尽量减少这些浪费性渲染


现在回到我们的案例:因为过滤器组件的 state 由其父组件掌控,所以我的推论是可能发生了不必要的渲染和计算。为了快速确诊,我们要使用 Chrome 调试工具。它有个 Paint Flashing 功能,可以将发生改变的 DOM 高亮显示。你可以在 Rendering 标签页临时激活该功能:

[译] 用 React Hooks 和调试工具提升应用性能[通俗易懂]_https://bianchenghao6.com/blog_idea_第3张

一经激活,浏览器就会显示哪些元素被重绘了。在本案例中效果如下:

[译] 用 React Hooks 和调试工具提升应用性能[通俗易懂]_https://bianchenghao6.com/blog_idea_第4张

看起来合情合理,只有我操纵的组件引发了 DOM 操纵。也就是说浏览器没有做不必要的绘制。那我们就要进一步深入来探究原因了。


为了把 React 组件重绘的情况看得更真切,我们得用 React 调试工具中一个类似的工具。它叫做 Highlight Updates,你可以在 React 调试工具的首选项面板中找到它。激活后,它会高亮显示所有正在渲染的组件。如果渲染时间过长,它还会用特殊颜色标识出来。

[译] 用 React Hooks 和调试工具提升应用性能[通俗易懂]_https://bianchenghao6.com/blog_idea_第5张

React 调试工具使你能够检查 React 组件层级,以及对应组件的 props 和 state。
它有浏览器插件(支持 Chrome 和 Firefox)和独立应用(支持 Safari、IE、和 React Native 等运行环境)两种形式。


这里就清晰地揭示了问题所在:当我在一个过滤器上拖拽,包含直方图的另一个过滤器也被重绘了。 这就是应该被避免的处理器资源浪费。像直方图这样的笨重组件尤其如此。

现在我们知道了问题所在,但还不知道导致界面响应缓慢的原因。为了找到原因,我们可以使用 Chrome 调试工具的 Performance 面板。它可以帮助你查看在浏览器在执行某一特定任务的过程中,每一帧具体做了什么。

关于 Performance 面板的使用细节,不在本文讨论范围之内。但你可以在这里找到教程。


我使用 Performance 面板记录了过滤器组件中的一次变更。放大火焰图后,我有了以下发现:

[译] 用 React Hooks 和调试工具提升应用性能[通俗易懂]_https://bianchenghao6.com/blog_idea_第6张

如你所见,这两个火焰图大体相同。第一张图(在 Timings 下方)展示了 React 组件的实际的加载和更新。React 调用了用户时间接口,所以我们能看到这张额外的图。第二张图展示了主线程上执行的所有任务,这张图更为详细。

我更喜欢用第一张图来看哪些组件性能差,用第二张图深入了解具体哪个函数和计算过程耗时更多。


第一次看到 Performance 面板的默认火焰图,你可能会被吓到。万幸 React 调试工具也有一个相似功能,在 Profiler 标签页中,能够根据用户时间接口生成同样的火焰图。我认为 React 调试工具中的火焰图更易于理解,而且它还有很多趁手的附加功能:

  • 你可以根据组件渲染时长将所有组件排序(见下方截图)。
  • 你可以快速浏览不同的渲染记录。
  • 你可以点击某组件查看特定渲染阶段的 props
[译] 用 React Hooks 和调试工具提升应用性能[通俗易懂]_https://bianchenghao6.com/blog_idea_第7张


以上图形揭露了罪魁祸首:Histogram。特别是渲染第二个直方图(右侧那个),耗费了很长时间(402.8 毫秒!),即使我根本没有拖拽它。我们破案了!接下来就该修复问题、优化组件性能了。

注意:我记录性能时打开了 CPU 节流功能,用 1/4 倍速模拟那些并非使用最新版 Macbook Pro 的用户,以此来突显性能问题。

提升组件性能

为防止浪费性渲染的发生,我们可以通过备忘技术优化组件。我们要使用 React.memo 来记忆组件,用 React 的备忘 hooks useMemouseCallback 记忆变量和函数。

React.memo

16.6.0 版本起,React 就支持高阶组件 React.memo 了。它等价于 React.PureComponent,但只适用于函数组件。社区正逐步从 class 的组件风格转向带有 hooks 的函数组件风格,而 React.memo 正是这种组件。

当你用 React.memo 包裹一个函数组件时,它会将传入的 props 进行浅层比较。当比较的 props 不一致时,才会重新渲染组件。你也可以自己写一个比较函数,作为第二个参数传入。但要慎用,以避免意外故障。

我们可以将组件分解为更小的组件,并把每个更小的组件都用 React.memo 包裹起来。如此你能保证当 props 更新,仅有组件的一部分重新渲染了。但也不要把所有东西都做备忘,因为比较 props 所花时间可能要比渲染组件的时间还要长。

在本文案例中,我用 React.memo 包裹了过滤器组件(RangeSlider)和 Histogram 组件。此外,我把直方图分解为包裹组件和 HistogramBuckets 组件两部分,将逻辑部分和展现部分剥离开来。

const RangeSlider = React.memo(props => {
   ...
});

备忘 hooks

React 16.8.0 版本为我们带来功能强大的 hooks,有了 hooks,我们可以轻松备忘组件中的值和回调函数。在引入 hooks 之前,你当然也可以用一个单独的库实现备忘功能,但自从它成为 React 原生库的一部分,集成和塑造工作流变得更加简单易行。

useMemo 会记忆一个值,这样就不用在下一轮渲染中重新计算它了。useCallback 记忆的则是回调函数。你可以给二者传入一个依赖数组,该数组包含了组件作用域的值(比如 props 和 state),这些值将在 hooks 内部被用到。每次渲染时,React 都会比较这些依赖值,一旦它们发生改变,React 就会更新备忘的值或函数。

注意:React 为了尽可能快地进行比较,使用了比较算法 Object.is 来优化比较的速度。也就是说,如果你把对象或数组的新实例作为 props 传入,比较时该算法会返回 false,并重新计算备忘的值。

传入备忘的 props

在本例中,未经使用 React.memo 的过滤器组件需要优化。这曾是父组件设置 props 的方式:

function handleChange(value => {
  ...
});

<RangeSlider  
  value={[minValue, maxValue]}  
  onChange={handleChange}
/>

每渲染一次,都要创建 handleChange 的一个实例,并传入一个新的数组实例作为 props。这就导致 RangeSlider 组件总是更新,尽管有 React.memo 包裹,因为 Object.is() 比较算法总是返回 false。为了精确优化,我得用下列代码重构:

const handleChange = useCallback((value) => {
    ...
}, []);

const value = useMemo(() => [minValue, maxValue], [minValue, maxValue]);

<RangeSlider  
  value={value}  
  onChange={handleChange}
/>

如果依赖数组为空,那么 handleChange 则仅在挂载时更新。无论 minValuemaxValue 何时更改,value 总会返回一个新数组。

我对 Histogram 组件做了同样的优化,Histogram 组件把 props 传到 HistogramBuckets 子组件中。

小提示:要想快速找出两次渲染中哪些 props 发生了变化,可以用这个精巧的 hooks:useWhyDidYouUpdate。

成果

通过方便快捷的优化,组件的性能得到了显著提升。经过备忘优化后,在相同的操作下,Histogram 组件的渲染时间缩短到了 0.5 毫秒。比起原来的 72.7 毫秒加上第二个直方图消耗的 402.8 毫秒,这可是超过千倍的提速啊!🤩 最终成果就是,仅用了极小的努力,就获得了更流畅的用户体验。

[译] 用 React Hooks 和调试工具提升应用性能[通俗易懂]_https://bianchenghao6.com/blog_idea_第8张


加入 C°F

另外,如果你被本文惊艳到了,CLEVER°FRANKE 的大门永远为达人敞开哦。来我们的招聘传送门看看,如果感兴趣,欢迎向我们展示你的超能力。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

发表回复