内容简介:在构建 React 应用时,你会发现随着嵌套组件增多,用户界面的某些部分开始变得缓慢迟滞。这是因为,被改变 state 的元素在组件树中的层级越高,浏览器需要重绘的组件越多。本文将告诉你如何在CLEVER°FRANKE 的一个客户端项目中,我做过一个过滤器组件,该组件包含一个展示步数的直方图。
在构建 React 应用时,你会发现随着嵌套组件增多,用户界面的某些部分开始变得缓慢迟滞。这是因为,被改变 state 的元素在组件树中的层级越高,浏览器需要重绘的组件越多。
本文将告诉你如何 通过备忘(memoization)技术避免不必要的重绘,让你的 React 应用快如闪电。:zap:
在CLEVER°FRANKE 的一个客户端项目中,我做过一个过滤器组件,该组件包含一个展示步数的直方图。
我发现每当拖拽过滤器的操纵杆,动画帧率就会骤降,导致组件失去效用。故此我决定一探究竟。
抽丝剥茧
只有明白用户拖拽操纵杆时的内部运作原理,才能确定从何处下手调试。React 使用虚拟 DOM 来代表 DOM 中真实的元素。每当用户操作界面元素,应用的 state 都会改变。React 会遍历所有受 state 改变影响的组件,计算生成新的虚拟 DOM。React 将新旧版本的虚拟 DOM 进行比较,若发现二者有差异,就将对应的变化更新到真实 DOM 上。该过程叫做reconciliation。
操纵 DOM 元素可是一个非常耗费资源的任务。同样,遍历所有受影响组件的 render 函数也很耗时, render
函数中的计算量很大时尤其如此。因此我们要尽量减少这些 浪费性渲染 。
现在回到我们的案例:因为过滤器组件的 state 由其父组件掌控,所以我的推论是可能发生了不必要的渲染和计算。为了快速确诊,我们要使用 Chrome 调试工具。它有个 Paint Flashing 功能,可以将发生改变的 DOM 高亮显示。你可以在 Rendering 标签页临时激活该功能:
一经激活,浏览器就会显示哪些元素被重绘了。在本案例中效果如下:
看起来合情合理,只有我操纵的组件引发了 DOM 操纵。也就是说浏览器没有做不必要的绘制。那我们就要进一步深入来探究原因了。
为了把 React 组件重绘的情况看得更真切,我们得用 React 调试工具 中一个类似的工具。它叫做 Highlight Updates ,你可以在 React 调试工具的首选项面板中找到它。激活后,它会高亮显示所有正在渲染的组件。如果渲染时间过长,它还会用特殊颜色标识出来。
React 调试工具使你能够检查 React 组件层级,以及对应组件的 props 和 state。
它有浏览器插件(支持Chrome 和Firefox)和 独立应用 (支持 Safari、IE、和 React Native 等运行环境)两种形式。
这里就清晰地揭示了问题所在:当我在一个过滤器上拖拽,包含直方图的另一个过滤器也被重绘了。这就是应该被避免的处理器资源浪费。像直方图这样的笨重组件尤其如此。
现在我们知道了问题所在,但还不知道导致界面响应缓慢的原因。为了找到原因,我们可以使用 Chrome 调试工具的 Performance 面板。它可以帮助你查看在浏览器在执行某一特定任务的过程中,每一帧具体做了什么。
关于 Performance 面板的使用细节,不在本文讨论范围之内。但你可以在这里找到教程。
我使用 Performance 面板记录了过滤器组件中的一次变更。放大火焰图后,我有了以下发现:
如你所见,这两个火焰图大体相同。第一张图(在 Timings 下方)展示了 React 组件的实际的加载和更新。React 调用了用户时间接口,所以我们能看到这张额外的图。第二张图展示了主线程上执行的所有任务,这张图更为详细。
我更喜欢用第一张图来看哪些组件性能差,用第二张图深入了解具体哪个函数和计算过程耗时更多。
第一次看到 Performance 面板的默认火焰图,你可能会被吓到。万幸 React 调试工具也有一个相似功能,在 Profiler 标签页中,能够根据用户时间接口生成同样的火焰图。我认为 React 调试工具中的火焰图更易于理解,而且它还有很多趁手的附加功能:
- 你可以根据组件渲染时长将所有组件排序(见下方截图)。
- 你可以快速浏览不同的渲染记录。
- 你可以点击某组件查看特定渲染阶段的 props 。
以上图形揭露了罪魁祸首: Histogram
。特别是渲染第二个直方图(右侧那个),耗费了很长时间(402.8 毫秒!),即使我根本没有拖拽它。我们破案了!接下来就该修复问题、优化组件性能了。
注意:我记录性能时打开了 CPU 节流功能,用 1/4 倍速模拟那些并非使用最新版 Macbook Pro 的用户,以此来突显性能问题。
提升组件性能
为防止浪费性渲染的发生,我们可以通过备忘技术优化组件。我们要使用 React.memo
来记忆组件,用 React 的备忘 hooks useMemo
和 useCallback
记忆变量和函数。
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
则仅在挂载时更新。无论 minValue
或 maxValue
何时更改, value
总会返回一个新数组。
我对 Histogram
组件做了同样的优化, Histogram
组件把 props 传到 HistogramBuckets
子组件中。
小提示:要想快速找出两次渲染中哪些 props 发生了变化,可以用这个精巧的 hooks:useWhyDidYouUpdate。
成果
通过方便快捷的优化,组件的性能得到了显著提升。经过备忘优化后,在相同的操作下, Histogram
组件的渲染时间缩短到了 0.5 毫秒。比起原来的 72.7 毫秒加上第二个直方图消耗的 402.8 毫秒, 这可是超过千倍的提速啊! 最终成果就是,仅用了极小的努力,就获得了更流畅的用户体验。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- WWDC 2018:效率提升爆表的 Xcode 和 LLDB 调试技巧
- 计算性能提升 100 倍!Uber 推出机器学习可视化调试工具 Manifold
- Uber 开源 AI 可视化调试工具 Manifold,2 个工作流让计算性能提升百倍
- iOS常用调试方法:断点调试
- 断点调试和日志调试之间的平衡点:函数计算调试之 Python 篇
- .NET高级调试系列-Windbg调试入门篇
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript DOM编程艺术 (第2版)
[英] Jeremy Keith、[加] Jeffrey Sambells / 杨涛、王建桥、杨晓云 等 / 人民邮电出版社 / 2011-4 / 49.00元
JavaScript是Web开发中最重要的一门语言,它强大而优美。无论是桌面开发,还是移动应用。JavaScript都是必须掌握的技术。W3C的DOM标准是开发Web应用的基石。已经得到所有现代浏览器的支持,这使得跨平台Web开发成了一件轻松惬意的事。 本书是超级畅销书的升级版,由倡导Web标准的领军人物执笔,揭示了前端开发的真谛,是学习JavaScript和DOM开发的必读之作。 本......一起来看看 《JavaScript DOM编程艺术 (第2版)》 这本书的介绍吧!