如何提高你的 React 应用的性能
如何提高你的 React 应用的性能
(How to greatly improve your React app performance)- Noam Elboim / from medium
本文旨在总结常见的性能缺陷,以及如何来避免这些缺陷。
性能问题在web应用开发中不是什么新鲜事。
我们每个人都有这样的时刻,当你把一个新的Component组件放到你的app中,你会突然发现你尝试的每一个用户交互动作都与期望的效果有很明显的滞后。有时,你可以重复使用多个同样的组件,这种尴尬的动效滞后会更加明显。像下面这样:
在那一刻你也许心里已经给写这个组件的人起了好几个绰号了。但是最好的办法是:做些什么,是的,你可以的!
我们将重点解决以下几个常见的 React 性能问题:
1.错误的 shouldComponentUpdate 实现 ,为什么 PureComponent 没能拯救你。
2.太快的改变 DOM。
3.滥用事件(events)和 回调(callbacks)。
对于以上的每个问题,我们先解释问题的根源,然后我们提出一些简单易用的方法来避免它。
管好你的 shouldComponentUpdate
组件的component 钩子函数 shouldComponentUpdate 的本意是用来阻止一些非必需的渲染(render), shouldComponentUpdate将即将更新的props和state作为参数,如果返回值是true, render函数就执行,否则不执行 render.
React.Component 默认的实现 shouldComponentUpdate 是返回true.
越多的render渲染意味着耗费越多的时间。所以我们需要防止不必要的更新来减少额外的时间。
为此,你会想到我们应该在实现 shouldComponentUpdate 的时候更谨慎些。
问题
让我们看一个简单的使用 shouldComponentUpdate 的例子:
Simple shallow implementation: 'this.props.children !== nextProps.children', but it's always returning true等下,为什么不起作用呢?
不起作用是因为 React 每次渲染的时候创建了一个新的 ReactElement!
这就意味着 在 shouldComponentUpdate函数中 Shallow Comparision 如:return this.props.children !== nextProps.children;几乎就相当于return true;
根据我的经验,大多数组件通常都以某种方式支持 ReactElement props(PropTypes.node or PropTyps.elemtn)比如像children这是很常见的情况。
那么, PureComonent又是怎样的呢?
React.PureComponent 是React.Component 的另外一种方式。它不是总在其 shouldComponent 实现中返回true,而是 props 和 state 的浅层比较。
使用 PureComponent 会返回同样的结果,如下:
PureComponent component is still always returning true这是 PureComponent 特性的bug吗?我不确定。我们需要知道的是,PureComponent 在大多数情况下不起作用,它并不能阻止一些不必要的更新。
可能的解决方案
我们第一点想到的是——进行深度比较! 这确实管用,但是它有两个重要缺陷:
1. 运行深度比较本身是一个过程比较长,比较重,比较耗时的动作。因此,在 shouldComponent 函数运行结束之后,render 函数才能运行。这样一来性能非但不能提升反而会变得更差。
2. 这只是基于当前的 React Elements 实现,在未来版本中可能会取消。
综上,在我看来,使用深度比较并不是一个好的解决办法。
为了寻找到更好的解决方案,我研究了一些其他的虚拟 DOM 库,看看他们是怎么解决这个问题的。
我发现了 Vue 作者Evan You 一个关于在Vue.js中添加 类React shouldeComponent 的 feature request 发表的一个有意思的评论。他解释到,这个问题并不能通过 "diffing" 虚拟DOM解决,因为它有很多未知的问题。依赖 React Elements 来检测组件中的状态变化并不是一个可行的解决方案。
在实际应用中,不应该在 shouldComponentUpdate 的实现中使用 React Elements 的比较作为返回结果。相反,应该使用某种状态的改变来告诉组件是否应该更新。
我们应该基于prop的不同来通知 state 的改变,而不是通过使用this.props.children !== nextProps.children。最好是一个数字或者字符串,这样比较会更快。
我们甚至可以使用一个新的 prop 专门用来通知组件是否应该更新。
更进一步,我和我的同事创建了一个高阶组件(HOC)。这个组件使用继承反转(Inheritance Inversion)来扩展通用的 shouldComponent 实现,也是 PureComponent 的替代方案。 而且确实有效。代码在这里:
https://github.com/NoamELB/shouldComponentUpdate-Children
必须说明的是,这只是一个通用的实现,所以并不是适用所有的情况。具体可以参考这里
例子在这里,使用了一个自定义的 shouldComponentUpdate 实现。正如上面提到的,它确实不会再进行不必要的渲染了。
几种比较:
允许你的组件扩张
你是否在你的应用中多次使用相同的组件,致使你的应用非常重动画也很卡顿,有时候即使使用一个也会导致应用性能的损耗?
问题
在创建复杂的组件时,你可能需要执行一些自定义 DOM 的操作。在创建的时候你可能会遇到两个问题:
1. 触发太多布局(Layout)而没有使用触发复合(Composite)或者重绘(Paint)
2. 太多没必要的Layout.多次读写DOM,导致 DOM不必要的重新计算。
让我们看下 原生 Collapse 组件,在0和内容高度之间改变它的高度。点击查看
当使用一个这样的组件时,可以正常展示。但是当你多次使用的时候......
如果你不是在移动设备上查看,可能感觉不明显。需要将你的chrome performance选项调到 6x slowdown
可能的解决方案
让我们分析下 Collapse 组件发生了什么——这是高度改变的时候的代码:
这里有两个问题需要注意:
1. 我们改变的height属性,根据csstriggers.com这个列表,改变高度(height)触发了布局(Layout)的重新计算。如果我们设法改变类似transform的东西,那只会触发Composite,并且会更平滑些,对吗?
事实正式如此,这样会表现更好,但是这样就会在Collapse组件下留下一个空白,因为我们没有改变它的高度。
2. 上面代码的第三行,这是常见的改变高度出发Layout的滥用:我们从DOM读取了高度this.contentEl.scrollHeight然后又通过this.containerEl.style.height对DOM设置了高度,然后多次重复这样的操作。
如果我们可以成组的一次性读取过来高度,然后再一次性设置高度,这样不是更好吗?
批量的读写 DOM 是一个很好的减少 Layout 的尝试。我们可以使用requestAnimationFrame对DOM 读写进行批量处理,像下面这样:
requestAnimationFrame能保证你的代码在浏览器下一帧触发,减少页面绘制成本,按需批量绘制。让你的动画更流畅。点击查看具体实现
这样用起来可能比较麻烦,那么可以使用内置组件或者使用第三方库比如Fastdom, Fastdom也是基于requesAnimationFrame 的原理通过批量处理DOM 读取/写入 操作来消除频繁的Layout操作。
值得一提的是,由于浏览器和设备功能的限制,有时您可能无法获得足够好的性能。在这些情况下,最好的解决方案可能是变更产品需求。
最后,你可能听过css的will-change属性。在特定的情况下它可以帮助你,但是使用不好也会有一定的风险。最好不要过度使用它。
管住你的 callbacks
当我们调用任何 DOM 事件的时候,有一个去抖(debounce)或者节流(throttle)函数时很有必要的。它可以让我们把这个函数的调用次数减少到我们想要的最低限度,以此来提高性能。
通常像这样写:window.addEventListener(‘resize’, _.throttle(callback)),但是为什么我们不能把它也运用到 React Components callbacks 里呢?
问题
让我们看下面这个组件:
有没有注意到,我们每次输入改变都会调用this.props.onChange, 它会被调用多次,虽然很多调用都是非必需的。如果父级正在根据onChange回调进行 DOM 更改或者任何其他比较繁重的操作,我们的应用会变得很卡顿。
可能的解决办法
其实我们可以这样改进:
Debounce the event现在,只有在用户输入完成后才调用props.onChange, 这样就阻止了很多不必要的事件操作。
另外相似的解决办法还有函数节流(throtle).点击查看throttle和debounce的区别。
总结
这些工具应该可以帮助您处理一些我们在React应用程序中遇到的性能问题。通过明智地使用shouldComponentUpdate,控制你对DOM做的改变,并通过debounce / throttle来延迟回调,你可以大大地提高你的应用程序的性能。
如果你想测试开发遇到的情况,请查看UiZoo。它是React组件的一个动态组件库,它可以解析你的组件并展示给你,让你可以开发,测试或与他人共享。
完整示例代码见:示例代码