04 February 2016 on React
本文是 React 性能工程系列文章的第一篇(共两篇) 第二篇 深入探讨React性能调试 译文 现在已经推出!
这篇文章适用于复杂的React应用。如果只是构建一些简单的、小型的应用,你还不用考虑性能问题。不必过早地优化,去构建吧!
然而,如果你是在构建一个DNA设计工具、一个胶体图片分析器、一个富文本编辑器,或者一个全能的电子数据表,你就会触碰到性能的瓶颈了。这时候,就有必要来解决这个问题了。在构建Benchling这个项目的过程中,我们遇到了很多问题。所以,本文的目的是给那些网络开发者和关注Benchling的粉丝分享我们学到的一些方法。(当然,如果你喜欢这类问题,我们正在招聘!)
在这篇文章中,我将会讲述使用React性能工具的一些基础知识、一些会导致React渲染瓶颈的常见问题,以及一些需要谨记的调试方法。
基准
浏览器性能可以用三句话来概述:理想中你期望浏览器每秒渲染60帧,每帧16.7毫秒。当你的app运行缓慢的时候,经常需要很长时间才能响应用户事件、处理数据或者重新渲染新的数据。大多数情况下,你并没有时刻在处理复杂的数据,只是浪费时间在重绘而已。
使用React, 不需要做额外的工作,就可以取得性能上的优势:
因为React会处理所有的DOM操作,很大程度上免去了DOM解析和布局所带来的问题。在后台,React会在JavaScript中维持虚拟DOM, 这样便于快速地把文档更新到期望状态。
我们要避免直接操作DOM,因为React组件的状态是储存在JS中的。一个传统的性能问题就是在不恰当的时刻操作DOM,这样会导致像被迫同步布局这样的问题(例如:为了获取某节点的样式 someNode.style.left
, 使得浏览器被迫渲染画面)。为了不用以下这种做法:
`someNode.style.left = parseInt(someNode.style.left) + 10 + "px";`
我们可以声明式地调用 `` 来触发组件,不需要从DOM元素读取数据,就可以简单地更新状态了。
`this.setState({left: this.state.left + 10}).`
说明一点,这些优化不用React也是可以实现的,我只是简单地指出React趋向于提前解决这些问题。
对于简单的应用,React 所带来的这些性能优化就足够了。我认为这些是使框架变得可行的最小工作量了。然而,当你开发的页面越来越多、越复杂时,维护和对比虚拟DOM就会变成一项昂贵的操作了。幸运的是,React提供了一些工具,可以检测哪里有性能问题,便于你及时地避开这些问题。
调试带来的性能问题
请注意 -- 调试本身也会带来一些问题,导致混淆调试部分,以为这部分不会留在生产中。
元素窗口
元素窗口是观察DOM元素是否被重新渲染的一个简单好用的途径,当一个属性改变或者一个DOM节点更新、插入、替换时,它都会闪现一个颜色。然而,元素面板的闪现,或者说是重新渲染也将影响到性能!经常我会从元素窗口切换到控制台,来更准确地感知每秒的帧数。
PropTypes
在用进行React开发时,当一个组件被渲染时,经常要进行PropType 校验。组件所接收到的 prop
先被检测来帮助调试和开发。使用 Chrome 提供的 JS Profiler
,你可以发现React组件在这个校验的方法上花费了很长一段时间。
尽管开发环境的警告提示有助于调试,但它们是会有一些性能方面的代价的,这些代价则不会反映在生产环境。有时我会使用切换到生产构建环境来忽略这种迟缓的错觉。(只要把NODE_ENV
改为 production
,就可以启动生产环境构建模式了:https://facebook.github.io/react/downloads.html#npm.)
通过React.addons.Perf来识别性能问题
在深入讲解常见问题的修复前,重点强调一下,你必须只花时间来修复你所能把控的那些问题。如果你毫无约束地乱优化是很容易走进死胡同的。啰嗦一下,应该专注于构建,并且只把时间花在修复主要的性能瓶颈上。
使用标准的调试工具来识别性能瓶颈仍然是可行的,但是经常很难来解释数据,因为实际应用的代码会比在React-land中的代码花费更多的时间(例如:你写的一个复杂的渲染方式运行得很快,但是其带来的虚拟DOM计算却是相当昂贵的)。这使我们很难在React-land中识别哪些应用代码导致了明显的瓶颈问题。
幸运的是,React自带一些性能检测工具,可以在React的非生产构建环境中使用(文档)。通过react/addons
,你可以找到对应的React.addons.Perf
。
我们可以这样写:
<IntermediateBinder
deleteItem={this.deleteItem}
boundArg={item.id}>
{(boundProps) => <TodoItem deleteItem={boundProps.deleteItem} />}
</IntermediateBinder>
(我们探索的另一个可能的做法是,使用一个自定义的绑定函数,这个函数本身储存了元数据, 它和一个更高端的检测函数结合使用,就可以检测到功能的结合实际上还没有改变。这似乎不能满足我们的需求。)
构造数组、对象字面量
这很简单,只是经常被忽略了。数组字面量会破坏 PureRenderMixin
:
> ['important', 'starred'] === ['important', 'starred']
false
如果你不希望这个对象被改变,你就可以把它放到一个模块常量或者组件静态变量中:
`const TAGS = ['important', 'starred'];`
子组件
在一个组件和它的子组件之间定义内容界限有利于性能优化----接口封装性良好的组件可以自然地促进性能更新。重构中间的组件可以帮助提高性能,你也可以使用 PureRenderMixin
来保存更新。
<div>
<ComplexForm props={this.props.complexFormProps} />
<ul>
<li prop={this.props.items[0]}>item A</li>
...1000 items...
</ul>
</div>
在上面这个例子中,如果 complexFormProps
和 items
来自同一个 store 的话,那么在 complexFormProps
里面输入,就会引发 store 的更新,而每个 store 的更新又会导致上面这整个实例的重新渲染。虚拟 DOM 的差异是很棒的,但仍然需要每次都检测。 然而,重构它的子组件,采用 this.props.items
,这样就只有当 this.props.items
变化时才会更新状态。
<div>
<CustomList items={this.props.items} />
<ComplexForm props={this.props.complexFormProps} />
</div>
缓存昂贵的计算
这个跟 状态来源单一性
原则有些相悖,但是如果 prop
中的计算是昂贵的,你就可以把它缓存在组件中。我们不必在渲染的方法中,直接地调用 doExpensiveComputation(this.prop.someProp)
,可以把这个函数进行封装,在prop 状态没改变的时候,把它缓存起来。
getCachedExpensiveComputation() {
if (this._cachedSomeProp !== this.prop.someProp) {
this._cachedSomeProp = this.prop.someProp;
this._cachedComputation = doExpensiveComputation(this.prop.someProp);
}
return this._cachedComputation;
}
后续的优化人员使用JS分析器,将可以很好地发现这个问题。
状态链接
React 的双向数据绑定对于简单的控制反转(IoC)非常有用,它允许子组件向父组件传递新的状态。如果对React表单组件只是使用 valueLink
的话是没那么糟糕的,因为 React 的表单输入是很简单的。但如果你像我们一样,在多个组件之间串联,那就会遇到问题了。状态链接实施如下:
linkState(key) {
return new ReactLink(
this.state[key],
ReactStateSetters.createStateKeySetter(this, key)
);
}
尽管状态没有改变,每调用一次 linkState
都会返回一个新的对象!这意味着 shallowCompare
永远不会起作用。不幸的是,我们的变通方案就是干脆不使用 linkState
。 如果不是要把一个 linkState
变成一个 getter prop
和一个 setter prop
的话,我们要避免创建一个新的对象。例如:nameLink={this.linkState(‘name')}
可以被替换成 name={this.state.name} setName={this.setName}
。(我们已经考虑写一个可以对自身进行缓存的 linkState
了)
编译程序的优化
新版的 Bebel 和 React 支持内联React元素并且自动提升常量。不幸的是,我们还没有用过这方面的技术,但它们将有助于减少 React.createElement
的调用, 以及加速DOM的更新和解。
总结
刚刚我们看了很多 (你应该看过原列表的!), 但是关键的两点就是你要习惯 profiling
和 shouldComponentUpdate
。 我希望这些都能够帮到你!
任何建议、评论等,如果我们错过了,欢迎通过 benchling.com 让我们知悉。
请继续关注本系列文章的第二篇,我们将讨论 React 的调试工作流,深入存在性能问题的代码实例,进而示范如何修复。
更新: 第二篇已经出来啦! Check it out - A Deep Dive into React Perf Debugging.
我们一如既往地欢迎喜欢我们产品的朋友来加入这个团队. :)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。