光速 React
Vixlet 团队优化性能的经验教训
在过去一年多,我们 Vixlet 的 web 团队已经着手于一个激动人心的项目:将我们的整个 web 应用迁移到 React
+ Redux
架构。对于整个团队来说,这是不断成长中的机会,在整个过程中,我们在这方面面临一些挑战。
因为我们的 web-app 可能有非常大的 feed 视图,包括成百上千的媒体、文本、视频、链接元素,我们花了相当多的时间寻找能充分利用 React
性能的方法。在这里,我们将分享我们这一路学到的一些经验教训。
声明:下面讲的做法和方法更适用于我们具体应用的性能需求。然而,像所有的开发者建议的那样,最重要的是要考虑到你的应用程序和团队的实际需求。React 是一个开箱即用的框架,所以你可能不需要像我们一样细致地优化性能。话虽如此,我们还是希望你能在这篇文章里找到一些有用的信息。
基本优化
向更大的世界迈出第一步。
render() 函数
一般来说,要尽可能少地在 render
函数中做操作。如果非要做一些复杂操作或者计算,也许你可以考虑使用一个 memoized 函数以便于缓存那些重复的结果。可以看看 Lodash.memoize,这是一个开箱即用的记忆函数。
反过来讲,避免在组件的 state
上存储一些容易计算的值也很重要。举个例子,如果 props
同时包含 firstName
和 lastName
,没必要在 state
上存一个 fullName
,因为它可以很容易通过提供的 props
来获取。如果一个值可以通过简单的字符串拼接或基本的算数运算从 props
派生出来,那么没理由将这些值包含在组件的 state
上。
Prop 和 Reconciliation
重要的是要记住,只要 props
(或 state
)的值不等于之前的值,React
就会触发重新渲染。如果 props
或者 state
包含一个对象或者数组,嵌套值中的任何改变也会触发重新渲染。考虑到这一点,你需要注意在每次渲染的生命周期中,创建一个新的 props
或者 state
都可能无意中导致了性能下降。
例子: 函数绑定的问题
例子: 对象或数组字面量
例子 : 注意字面量的回退值
尽可能的保持 Props(和 State)简单和精简
理想情况下,传递给组件的 props
应该是它直接需要的。为了将值传给子组件而将一个大的、复杂的对象或者很多独立的 props
传递给一个组件会导致很多不必要的组件渲染(并且会增加开发复杂性)。
在 Vixlet,我们使用 Redux 作为状态容器,所以在我们看来,最理想的是方案在组件层次结构的每一个层级中使用 react-redux 的 connect() 函数直接从 store
上获取数据。connect
函数的性能很好,并且使用它的开销也非常小。
组件方法
由于组件方法是为组件的每个实例创建的,如果可能的话,使用 helper/util
模块的纯函数或者静态类方法。尤其在渲染大量组件的应用中会有明显的区别。
进阶
在我看来视图的变化是邪恶的!
shouldComponentUpdate()
React 有一个生命周期函数 shouldComponentUpdate()。这个方法可以根据当前的和下一次的 props
和 state
来通知这个 React 组件是否应该被重新渲染。
然而使用这个方法有一个问题,开发者必须考虑到需要触发重新渲染的每一种情况。这会导致逻辑复杂,一般来说,会非常痛苦。如果非常需要,你可以使用一个自定义的 shouldComponentUpdate()
方法,但是很多情况下有更好的选择。
React.PureComponent
React 从 v15
开始会包含一个 PureComponent
类,它可以被用来构建组件。React.PureComponent
声明了它自己的 shouldComponentUpdate()
方法,它自动对当前的和下一次的 props
和 state
做一次浅对比。有关浅对比的更多信息,请参考这个 Stack Overflow:
http://stackoverflow.com/questions/36084515/how-does-shallow-compare-work-in-react
在大多数情况下,React.PureComponent
是比 React.Component
更好的选择。在创建新组件时,首先尝试将其构建为纯组件,只有组件需要更多功能时才使用 React.Component
。
更多信息,请查阅相关文档 React.PureComponent。
组件性能分析(在 Chrome 里)
在新版本的 Chrome 里,timeline
工具里有一个额外的内置功能可以显示哪些 React 组件正在渲染以及他们花费的时间。要启用此功能,将 ?react_perf
作为要测试的 URL 的查询字符串。React 渲染时间轴数据将位于 User Timing 部分。
更多相关信息,请查阅官方文档:Profiling Components with Chrome Timeline 。
有用的工具: why-did-you-update
这是一个很棒的 NPM 包,他们给 React 添加补丁,当一个组件触发了不必要的重新渲染时,它会在控制台输出一个 console
提示。
注意: 这个模块在初始化时可以通过一个过滤器匹配特定的想要优化的组件,否则你的命令行可能会被垃圾信息填满,并且可能你的浏览器会因此而挂起或者崩溃,查阅 why-did-you-update 文档 获取更多详细信息。
常见性能陷阱
setTimeout() 和 setInterval()
在 React 组件中使用 setTimeout()
或者 setInterval()
要十分小心。几乎总是有更好的选择,例如 ‘resize’ 和 ‘scroll’ 事件(注意:有关注意事项请参阅下一节)。
如果你需要使用 setTimeout()
和 setInterval()
,你必须 遵守下面两条建议
不要设置过短的时间间隔。
当心那些小于 100 ms 的定时器,他们很可能是没意义的。如果确实需要一个更短的时间,可以使用 window.requestAnimationFrame() 替代。
保留对这些函数的引用,并且在 unmount 时取消或者销毁他们。
setTimeout()
和 setInterval()
都返回一个延迟函数的引用,并且需要的时候可以取消它们。由于这些函数是在全局作用域执行的,他们不在乎你的组件是否存在,这会导致报错甚至程序卡死。
注意: 对 window.requestAnimationFrame()
来说也是如此
解决这个问题最简答的方法是使用 react-timeout 这个 NPM 包,它提供了一个可以自动处理上述内容的高阶组件。它将 setTimeout/setInterval 等功能添加到包装组建的 props
上。(特别感谢 Vixlet 的开发人员 Carl Pillot 提供这个方法)
如果你不想引入这个依赖,并且希望自行解决此问题,你可以使用以下的方法:
如果你使用 requestAnimationFrame() 执行的一个动画循环,可以使用一个非常相似的解决方案,当前代码要有一点小的修改:
未 去抖 频繁触发的事件
某些常见的事件可能会非常频繁的触发,例如 scroll
,resize
。去抖这些事件是明智的,特别是如果事件处理程序执行的不仅仅是基本功能。
Lodash
有 _.debounce 方法。在 NPM 上还有一个独立的 debounce 包.
“但是我真的需要立即反馈 scroll/resize 或者别的事件”
我发现一种可以处理这些事件并且以高性能的方式进行响应的方法,那就是在第一次事件触发时启动 requestAnimationFrame()
循环。然后可以使用 [debounce()](https://lodash.com/docs#debounce)
方法并且将 trailing
这个配置项设为 true
(这意味着该功能只在频繁触发的事件流结束后触发)来取消对值的监听,看看下面这个例子。
密集CPU任务线程阻塞
某些任务一直是 CPU 密集型的,因此可能会导致主渲染线程的阻塞。举几个例子,比如非常复杂的数学计算,迭代非常大的数组,使用 File
api 进行文件读写,利用 对图片进行编码解码。
在这些情况下,如果有可能最好使用 Web Worker
将这些功能移到另一个线程上,这样我们的主渲染线程可以保持顺滑。
相关阅读
MDN 文章: Using Web Workers
MDN 文档: Worker API
结语
我们希望上述建议对您能有所帮助。如果没有 Vixlet 团队的伟大工作和研究,上述的提示和编程技巧是不可能产出的。他们真的是我曾经合作过的最棒的团队之一。
在你的 React 的征途中保持学习和练习,愿原力与你同在!