React学习7—— 高阶应用:性能优化

性能优化

在React内部已经使用了许多巧妙的技术来最小化由于Dom变更导致UI渲染所耗费的时间。对于很多应用来说,使用React后无需太多工作就会让客户端执行性能有质的提升。然而,还是很其他更多的办法来加速React程序。

使用生产模式来构建应用

如果在开发和使用的过程中感觉了React应用有明显的性能问题,请先确认是否已经构建了压缩后的生产包:

  • 在单页面用中,打包之后的生产文件应该是.min.js版本。
  • 对于Brunch(html打包工具:http://brunch.io/),打包命令需要包含-p标记。
  • 对于Browserify(UMD规范打包工具:http://browserify.org/),打包时需要增加生产配置参数——NODE_ENV=production
  • 对于在创建React App时,需要执行npm run build命令,并按照说明操作。
  • 对于Rollup(JavaScript代码高效压缩工具:https://rollupjs.org/),生产打包时需要在commonjs插件之前使用replace插件:
    plugins: [
      require('rollup-plugin-replace')({
        'process.env.NODE_ENV': JSON.stringify('production')
      }),require('rollup-plugin-commonjs')(),// ...
    ]

    可以在这里看到 一个完整的例子:see this gist

  • 使用Webpack打包,需要在打生产包的配置脚本中增加以下配置和插件:

    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),new webpack.optimize.UglifyJsPlugin()

切记不要将开发模式的包发布到生产环境,因为开发包中额外包含了许多用于辅助的测试的信息,无论在加载还是执行时,它都比较慢。

使用chrome分析组件的渲染时间线

在开发模式中下你可以直接在chrome的性能工具中看到组件是如何装载、更新和卸载的。例如下面的图片展示的效果:

在chrome中按照以下步骤执行:

  1. 使用?react_perf作为url参数(例如:http://localhost:3000/?react_perf)
  2. 打开chrome的开发工具Timeline,然后点击Record(左上角的红色按钮)。
  3. 执行你要监控的操作。请不要记录超过20秒,这可能会导致chrome假死。
  4. 停止记录。
  5. React事件将会批量记录在User Timing标签里。

关于分析的数据,需要明确的是:渲染的时间只是一个相对的参考值,在构建成生产包之后,渲染的速度会更快。尽管如此,这些数据仍然能够帮助我们分析是否有不相关的UI被错误的更新,以及UI更新的频率和深度。

目前只有Chrome、Edge和IE支持这个特性,但是官方正在使用User Timing API标准让更多浏览器支持这个特性。

手工避免重复渲染

React构建和维护了一个内部的虚拟Dom,这个Dom和真实的UI是相互映射的关系,他包含从用户自定义组件中返回的各种React元素。这个虚拟的Dom使得React可以避免重复渲染相同的Dom节点并在访问存在的节点时直接使用React的虚拟层数据,这样设计的原因是重复渲染浏览器或web view的UI比操作一个JavaScript的对象要慢许多。在React Native也采用同样的处理方式。

当组件的props和state变更时,React会将最新返回的元素与之前旧的元素进行对比来确定是否真的需要重新渲染真实的Dom。当他们不相等时,React会更新真实的Dom。

在某些情况下,可以在自定义组件中重载shouldComponentUpdate方法来加速触发渲染的比对的过程。该方法的默认实现返回参数为true,此时React将按照原来的方式进行比对和渲染:

shouldComponentUpdate(nextProps,nextState) {
  return true;
}

如果在某些情况下能够清晰的明确组件不需要重新渲染,可以在shouldComponentUpdate方法中返回false,这样会让让组件跳过整个渲染过程,包括不再调用当前组件和子组件的render()方法。

shouldComponentUpdate 的执行过程

下面是一个组件结构树。图中,“SCU”表示shouldComponentUpdate方法返回的值(绿色true,红色fasle),“vDOMEq”表示React的匹配是否一致(绿色true,红色fasle),有颜色的红圈表示是否执行了UI重绘(绿色表示没重绘,红色表示执行重绘)。

在C2组件中,shouldComponentUpdate方法返回了false,所以React不会判断是否需要重新渲染C2并且不执行render()方法,因此在C4和C5中不再执行shouldComponentUpdate方法。

对于C1和C3,shouldComponentUpdate都返回了true,所以React必须对着2个组件进行比对。对于C6,shouldComponentUpdate返回true,而且比对的结果是需要UI重绘,因此C6会更新他们的真实Dom。

还有一个值得关心的组件是C8,React在这个组件中执行了render()方法,但是由于虚拟Dom并没有发生变更,前后比对一致,所以并没有发生真实Dom渲染。

在整个过程中React仅仅变更了C6组件的UI样式,C8由于前后虚拟Dom一致因此没有真正的执行UI渲染。C2、C2的子组件以及C7没有执行render()方法。

一个shouldComponentUpdate的例子

在例子中,当props.color和state.count发生变更时进行UI渲染,我们在shouldComponentUpdate方法中进行检查:

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps,nextState) {
    //只判断props.color和nextState.count是否变更,其他情况均不渲染
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

在这段代码中,shouldComponentUpdate仅仅检查props.colorstate.count是否发生变更,如果他们的值没有修改,组件将不会发生任何更新。在实际使用中,组件往往比这个复杂,我们可以使用类似于“浅比较”(关于浅比较可以参看:Shallow Compare)的模式来比对所有的属性或状态是否发生变更。React提供了这个模式的一个实现组件,只要让组件继承自React.PureComponent即可。我们可以将代码进行下面的修改:

//继承自React.PureComponent
class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

在大部分情况下,只要使用React.PureComponent就可以代替我们自己重载shouldComponentUpdate方法,但是它仅仅适用于“浅比较”,所以这个组件不适用于props和state数据发生突变的情况。

附:数据突变(mutated)是指变量的引用没有改变(指针地址未改变),但是引用指向的数据发生了变化(指针指向的数据发生变更)。例如const x = {foo:'foo'}。x.foo='none' 就是一个突变。

在更复杂的数据结构中还会存在一些问题。例如下面的代码,我们希望ListOfWords组件将words参数渲染成一个逗号分隔的字符串,而父组件监控点击事件,每次点击都会增加一个单词到列表中,但是下面的代码并不会正确工作:

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 这段内容会导致代码不按照预期工作。
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

导致代码无法正常工作的原因是PureComponent仅仅对 this.props.words的新旧值进行“浅比较”。在words值在handleClick中被修改之后,即使有新的单词被添加到数组中,但是this.props.words的新旧值在进行比较时是一样的(引用对象比较),因此ListOfWords一直不会发生渲染。

非突变数据的价值

有一个简单的方法预防上面提到的问题,就是在使用prop和state时防止数据发生突变。例如下面的例如,我们用数组的concat方法来代替等号“=”,这样在concat后会产生一个新的数组赋值给this.state.words:

handleClick() {
  this.setState(prevState => ({
    words: prevState.words.concat(['marklar'])
  }));
}

ES6支持列表扩展语法,因此我们更容易在es6中实现非突变的数据赋值,例如:

handleClick() {
  this.setState(prevState => ({
    words: [...prevState.words,'marklar'],}));
};

可以重写传统的赋值语句防止对象中的数据发生数据突变。下面的例子有一个名为colormap的对象,我们想在修改colormap.right的值时渲染组件,我们可以这样重写组件:

function updateColorMap(colormap) {
  colormap.right = 'blue'; //浅拷贝,指针地址未变,数据发生变化。
}

可以使用Object.assign方法来防止数据突变:

function updateColorMap(colormap) {
  // 深拷贝,修改返回对象的地址
  return Object.assign({},colormap,{right: 'blue'});
}

修改后updateColorMap方法返回一个新的实例。需要注意的是某些浏览器不支持Object.assign方法,我们需要使用polyfill(差异化抹平,比如我们引入了babel-polyfill)来解决这个问题。

有一个新的JavaScript方案是使用 扩展传播特性(见 object spread properties)来解决数据突变问题,实现如下:

function updateColorMap(colormap) {
  return {...colormap,right: 'blue'};
}

如果是构建React的App应用,那么以上方法都能够很好的支持,如果是在浏览器环境使用,需要引入polyfill机制。

使用不可变的数据结构

Immutable.js是解决数据突变问题的另外一种解决方案。它提供不可变、持久化的集合。集合包含下列结构:

  • Immutable:一旦数据被创建,改集合不能在任何其他地方修改。
  • Persistent:可以从已有的的数据集合(例如set)来创建新的数据集合。在创建新的数据集合后,已有的数据集合依然有效。
  • 结构分享(Structural Sharing):使用和原始数据尽可能相似的结构创建新的数据集合,并将复制降至最低,尽可能的提高效率。

数据结构不可变的特性使跟踪数据变化变得很简单。任何变更将始终导致创建一个新的对象,所以我们只需要检查引用(指针地址)是否已经被修改即可确定数据是否已经修改。例如在常规的JavaScript代码中:

const x = { foo: "bar" };
const y = x;
y.foo = "baz";
x === y; // true

尽管y的值已经被修改,但是它和x都是同一个引用(指向相同的地址),因此最后的比较语句会返回true。我们可以使用immutable.js来修改代码:

const SomeRecord = Immutable.Record({ foo: null});
const x = new SomeRecord({ foo: 'bar'});
const y = x.set('foo','baz');
x === y; // false

在这个例子中,由于x突变时使用了新的引用,我们可以安全的假设x已经发生改变。

还有两个库可以帮我们构建不可变数据:seamless-immutableandimmutability-helper

不可变的数据结构为我们跟踪数据对象变更提供了更加简便的方式,这是我们快速实现shouldComponentUpdate方法的基础。使用不可变数据后,可以为React提供不错的性能提升。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom