【译】精挑细选的一份React性能问题优化的清单

原文 Death by a thousand cuts - a checklist for eliminating common React performance issues

github的地址 欢迎star

前言(可以略过)

我们今天将会用一个具体的例子一步步的解决React的一些常见的性能问题。 你想知道怎样让你的React项目运行更快吗? 如果是,请继续往下看! 此时如果有一份常见的React性能优化清单是多么方便,没错,在这里就有一份!

首先,我会直接给你看项目中问题,并给出问题相应的解决方法。这样做,就和我们实际上的项目差别不大了(在一些概念上)。

这篇文章并不是长篇大论,相反地,我们讨论一些东西都是今后你们马上就能用到的。

一个示例项目

为了是文章讲的尽可能真实,我通过这个简单的React应用(名字叫 Cardie的app)带你经历各种实际的用户场景。

叫 Cardie 的手机应用 github源码地址

Cardie仅展示了用户信息,通常称呼为用户档案卡。当然,它也提供一个按钮,点击改变用户的一些信息。

点击app底部的按钮后,用户的信息就会被改变

尽管它如此的简单,一点都不是app,但当你通过这个例子寻找并解决性能问题之后,真实环境中App的性能问题你也能随之解决。 请保持耐心与放松。下面正式介绍优化清单!

1. 辨别无用浪费的渲染

辨别无用浪费的渲染,是这份清单的一个良好开端。 有几种不同的方法解决辨别问题,最简单方法,通过React dev tools工具实现,点击设置按钮,勾选“highlight updates”选项,就可以清楚看到哪些组件更新了。

当用户和app交互的时候,在屏幕上更新的组件就会出现绿色的闪光光罩。

对于Cardie,改变用户信息后,似乎整个App组件都重新渲染了

注意环绕用户卡片信息的绿色的闪光光罩。

这似乎是不应该的。

当app运行的时候,只改变组件一小点地方,不应该导致整个组件的的重新渲染。

实际更新应该发生在App组件的一小部分

更理想的高亮更新显示应该是这个样子:

注意现在的高亮更新只显示包含了一小块更新的区域

在更复杂的应用中,无效浪费渲染的影响是巨大的!解决了重新渲染问题足够提升应用的性能了。

看完这个问题,对此你有什么解决办法?

2. 将需要频繁更新的区域抽离成独立的组件

一旦你注意到了你应用中无用的渲染,从你的组件树中拆分出频繁更新的区域是一个不错的开始。

下面具体说明如何拆分。

在 Cardie中,App组件通过react-reduxconnect函数连接到了redux store。从store中,它接受的props有:namelocationlikes以及description

<App/>需要直接从redux store中接受的props

description的props定义了当前用户的信息。

本质上,无论何时点击按钮改变了用户信息,description的值都会改变。它的改变导致了整个App组件的重新渲染。

你是否想起了React官网中说的,每当组件的 propsstate改变时,都会触发该组件重新渲染。

一个react组件会渲染一个由 propsstate元素定义的组件树。如果propsstate改变,这个状态组件树就会重新生成一个新的树

此时我们要把需要更新的组件单独拆分出来渲染,而不是整个App组件毫无意义的重新渲染。

例如,我们可以创建一个叫Profession组件渲染自己的DOM元素。

这样的话,Profession组件就能渲染用户信息的description 的数据,列如I am a Coder

<Profession/><App/>中渲染

此时组件树看起来是这样:

对于profession的props,我们关注的重点不再是<App/>,而是变成了<Profession/>

profession数据由<Profession/>组件直接从redux store获取

不管你是否使用redux,这时改变profession的props,将不再导致App的重新渲染,而<Profession/>将重新渲染。

完成这个重构之后,你将得到理想的高亮更新显示:

想在高亮更新仅包含<Profession />

为了查看代码的更改,请从远程仓库克隆切换分支到 isolated-component branch查看

3. 适当地使用纯组件

任何提到React性能的地方都会提及到pure components

然而,你知道怎样在合适的时候使用纯组件吗?

当然,我们可以把每个组件都变成纯组件,但请记住不要对外层的容器组件这样操作。因为还有shouldComponentUpdate函数。

纯组件没有自己的state,因此,纯组件的重新渲染仅仅由props改变导致的。

一个简单的实现纯组件的方法是用React.PureComponent代替React.Component

React.PureComponent代替React.Component

用插画展示这个特定的用法,把Profession拆分成粒度更小的组件。

这是拆分之前的Profession组件

const Description = ({ description }) => {
  return (
    <p>
     <span className="faint">I am</span> a {description}
    </p>
  );
}
复制代码

这是拆分之后的组件:

const Description = ({ description }) => {
  return (
    <p>
      <I />
      <Am />
      <A />
      <Profession profession={description} />
    </p>
  );
};
复制代码

现在Description组件就渲染4个子组件。

注意 Description组件有 profession的prop,这个prop只传递给了 Profession,其它3个组件并不关心 profession的值。

当然这些新组件的内容是极其简单的。例如,<I />组件仅仅渲染了一个span >I </span>元素。

有趣的是每当description的prop改变之后,Profession的每个子组件都会重新渲染。

每当Description接受一个新的prop值,它的所有子组件都会重新渲染

我在每个子组件的render方法添加了logs日志,你能够确实看到每个子组件都被重新渲染了。

你可以用 react dev tools查看高亮更新部分

这个行为是符合预期的。每当组件的props或者state改变,组件渲染的树就会重新计算。

在这个例子中,你应该认同让组件<I/>, <Am/> 以及 <A/>重新渲染是没有任何意义的。尤其是在你的项目足够的大的时候,这将产生性能问题。

那么怎么把子组件变成纯组件呢?

针对<I/>组件

import React, {Component, PureComponent} from "react"

//before 
class I extends Component {
  render() {
    return <span className="faint">I </span>;
}

//after 
class I extends PureComponent {
  render() {
    return <span className="faint">I </span>;
}
复制代码

这样修改后,当这些子组件中prop的值没有改变时,react就不会重新渲染这些组件。

对于这个例子,即使你改变了props的值,<I/>, <Am/> 以及 <A/>这些组件也不会重新渲染!

在重构之后观察高亮更新的显示,你会发现<I/>, <Am/> 以及 <A/>这些组件都没有更新,仅仅Profession组件改变了,因为它的prop改变了。

<I/>, <Am/> 以及 <A/>组件没有显示高亮更新

在大型项目中,把某些组件改成纯组件你会发现巨大的性能提升。

为了查看代码的更改,请从远程仓库克隆切换分支到 pure-component branch查看

4. 避免通过一个新的对象作为props

重复一遍,每当一个组件的props改变时,就会重新渲染这个组件。

当props或者state改变时,组件tree就会返回一个新的

如果你的组件的props没有改变,但React认为它改变了,会怎样呢?
它们也会重新渲染!
是不是困惑的?
出现这种异常情况,你需要知道JavaScript是怎么工作的,以及React是怎么对比旧的和新的prop值的变化的。
来看看这个例子。
Description组件的内容:

const Description = ({ description }) => {
  return (
	<p>
	   <I />
	   <Am />
	   <A />
 	   <Profession profession={description} />
	</p>
  );
};
复制代码

接下来,我们会重构I组件,给他传入了命名为i的prop,作为一个表单提交的属性对象:

class I extends PureComponent {
  render() {
    return <span className="faint">{this.props.i.value} </span>;
  }
}
复制代码

Description组件中,以如下的方式创建了i并传递给I组件:

class Description extends Component {
  render() {
	const i = {
	  value: "i"
	};
	return (
	  <p>
            <I i={i} />
	    <Am />
	    <A />
	    <Profession profession={this.props.description} />
	  </p>
	);
  }
}
复制代码

接下来请耐心听我的解释,
这是完全错误的代码,但它能够正常运行,除了有一个问题。
尽管I组件是一个纯组件,但只要用户的职业这个属性改变了就会重新渲染I

点击按钮,<I/> and<Profession/>组件都重新渲染了。事实上,props并没有发生改变,为什么重新渲染了呢?

为什么?
一旦Description组件接受了一个新的props,就会调用render生成一个React的组件树。

在调用render函数之后,ta就会创建一个新的i的实例:

const i = { 
  value: "i"
};
复制代码

当React执行到<I i={i} />这里的时候,它认为i已经变了,因为它是一个新的对象(引用的对象变了),因此重新渲染了。

React判断当前的props和新的props过程的是浅比较

基本类型的数据像字符串和数字就是比较他们的值,而对象是比较它们的引用是否相等。
即使i的值改变前后是一样,但它指向的引用对象变了(在内存中的位置不相同),所以会重新渲染。
每次调用render,就会新生成一个对象。<I/>中prop的i就会被当做新的,导致重新渲染。

在更大的应用中,它就会导致无效的渲染,造成潜在的性能问题。
应该避免这样。
在应用中prop也会包含事件处理。
如果要避免性能浪费,那么不应这样:

... 
render() {
  <div onClick={() => {//do something here}}
}
...
复制代码

每次render都会产生一个新的函数对象。应该像这样:

...
handleClick:() ={
}
render() {
  <div onClick={this.handleClick}
}
...
复制代码

明白了吗? 同理,我们如下重构<I />

class Description extends Component {
  i = {
    value: "i"
  };
  render() {
    return (
      <p>
       <I i={this.i} />
       <Am /> 
       <A />
       <Profession profession={this.props.description} />
      </p>
    );
  }
}
复制代码

现在,在<I i={this.i} />i的引用都是this.i 。调用render就不会产生新的对象。

为了查看代码的更改,请从远程仓库克隆切换分支到 new-objects branch查看

5. 使用生产模式构建打包

部署到生产环境,应该要使用React的生产模式。它是简单的却是最好的实践。

在开发模式打包,react开发者工具就会弹出警告

如果你使用了create-react-app的脚手架,运行生产模式打包,仅需运行命令: npm run build。 它将会尽可能优化压缩你的代码。

6. 使用代码分割(code splitting)

当你打包你的应用,你可能会把整个项目打包成一个大的块。

此时的问题,当你的应用变大的时候,那个块也会变大。

当用户访问网站,他就会获取到整个项目的一个大块

代码分割提倡的不是用户立即就获取到项目的整个块,而是用户需要的时候,才动态的加载相关的项目块。

一个常见的例子就是通过路由来代码分割。这个方法,代码将会根据路由划分成不同的块。

/home路由被划分成了一个小块,/about路由也一样

也还有其他代码分割的方法。比如,如果一个组件当前对用户来说是不可见的,那它就可以延迟加载,当用户需要的时候在渲染。

无论你选择哪一种方法,重要的是做好权衡,不要降低了用户体验。

代码分割是极好的,它能提高你应用的性能。

我仅仅从概念上解释了代码分割的知识。如果你想知道更多代码分割的知识,请查看React的官方文档,很好的解释了代码分割的知识。

结尾

现在你已经获取到一份还算可以的追踪修复性能问题的清单了。快来让你的应用更快吧!

广告略过

如果有错误或者不严谨的地方,请务必给予指正,十分感谢!

猜你喜欢

转载自juejin.im/post/5c905d166fb9a070c85902be