3

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> 

在上面这个例子中,如果 complexFormPropsitems 来自同一个 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的更新和解。

总结

刚刚我们看了很多 (你应该看过原列表的!), 但是关键的两点就是你要习惯 profilingshouldComponentUpdate。 我希望这些都能够帮到你!

任何建议、评论等,如果我们错过了,欢迎通过 benchling.com 让我们知悉。

请继续关注本系列文章的第二篇,我们将讨论 React 的调试工作流,深入存在性能问题的代码实例,进而示范如何修复。

Hacker News的讨论

更新: 第二篇已经出来啦! Check it out - A Deep Dive into React Perf Debugging.

我们一如既往地欢迎喜欢我们产品的朋友来加入这个团队. :)


grace_xhw
1.4k 声望132 粉丝

博观而约取,厚积而薄发