React性能探索 --- 避免不必要渲染

背景

上一篇文章的结尾 https://cloud.tencent.com/developer/article/1009611 我们说到,也许,不是所有的节点都需要重新渲染,对于那些不需要渲染的节点,我们如何找到它们并做优化呢?

本篇文章来具体解答这个问题。

应用分析

首先,先看这个应用:页面的两部分分别渲染5000个节点,从1-5000。当点击按钮之后,第二部分的节点会更新,重新渲染从2-5001的数字,但是第一部分保持不变。

import React, { createElement, Component } from 'react';
import {render} from 'react-dom';
import Perf from 'react-addons-perf';
import ListItem from './ListItem'

function arrayGenerator(length) {
  return Array.apply(null, { length: length }).map(Number.call, Number)
}


class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      multiplier: 1
    }
  }

  resetMultiplier() {
    this.setState({ multiplier: 2 })
  }

  render() {
    return (
      <div className="App">
        <button onClick={this.resetMultiplier.bind(this)}>Click Me</button>
        <ul>
          {
            arrayGenerator(5000).map(i => {
              return <ListItem key={i} text={i}/>
            })
          }
          {
            arrayGenerator(5000).map(i => {
              return <ListItem key={i} text={i + this.state.multiplier}/>
            })
          }
        </ul>
      </div>
    );
  }
}

render(<App />,document.getElementById('main'));

gitbug 链接: https://github.com/hhhuangqiong/performance-for-react

感兴趣的同学可以下载跑一跑代码

分析更新时间

这里用react的Perf工具来测量重新渲染的时间。

使用方法:

npm install --save-dev react-addons-perf
import Perf from 'react-addons-perf'

这里主要用到四个方法:

  • Perf.start():开始计时
  • Perf.stop():结束计时
  • Perf.printInclusive():打印组件总的渲染时间
  • Perf.printWasted():打印浪费的时间

开始计时的函数,我把它放到resetMultiplier里,即将发生发生改变时开始计时。然后在componentDidUpdate里,用Perf.stop()结束计时,然后打印渲染组件的时间跟浪费的时间。

  componentDidUpdate() {
    Perf.stop()
    Perf.printInclusive()
    Perf.printWasted()
  }

  resetMultiplier() {
    Perf.start()
    this.setState({ multiplier: 2 })
  }

当我们点击按钮,可以看到控制台打印出下面的信息:

由控制台的数据可以看出,App用了90.59ms渲染,其中渲染ListItem的时间为55ms,渲染了10000次,其中有5000次是浪费的,因为这部分页面的内容完全没有更新的改动。

如何修复

既然是不需要渲染,那就要阻止它的渲染。React给我们提供了一个方法shouldComponentUpdate(),当这个方法返回true的时候,需要重新渲染,false的时候不需要(默认是true).

在这个栗子中,只要text的值不变,就不需要重新渲染。所以,可以这样改写ListItem 的shouldComponentUpdate

import React, { Component } from 'react'

export default class ListItem extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.text !== this.props.text  
  }

  render() {
    let { text } = this.props
    return <li>{text}</li>
  }
}

在重新点击一下按钮,在控制台可以发现

App总的渲染时间降到了62.14ms,并且ListItem只重新渲染了5000个节点,完全消除了浪费的渲染。

对于上面的写法,React提供了一个新的组件PureComponent来做这件事,它会自动浅对比props/state,当两者相同的时候不渲染节点。所以,listItem又可以改写成

import React, { PureComponent } from 'react'

export default class ListItem extends PureComponent {
  render() {
    let { text } = this.props
    return <li>{text}</li>
  }
}

跑一跑代码

通过控制台可以看到达到的效果是一样的(有点误差是正常的)。

这里再安利一个可以发现应用里是否存在不该重新渲染的节点工具:why-did-you-update

使用方法

npm i --save-dev why-did-you-update
import React from 'react'
if (process.env.NODE_ENV !== 'production') {
  const {whyDidYouUpdate} = require('why-did-you-update')
  whyDidYouUpdate(React)
}

然后点击按钮看控制台

可以看到不应该重新渲染的节点出现了Value did not change. Avoidable re-render!的警告,是不是很实用!

注意的点

PureComponent只会浅比较,所以不适合用于深层嵌套的对象。同时,PureComponent不仅仅会跳过自己的重新渲染,还会跳过它所有子节点的,所以要注意,用它的时候是最好没有子节点并且不依赖于global state的展示型组件。

与Staleless的关系

不知道有没有人跟我有这样的疑问,无状态组件跟纯净组件有什么不同?这里做一个区分:

无状态组件只是作为一个展示组件,它的好处是:

  • 易复用,易测试
  • 与逻辑处理数据层解耦,一般来说,app里有越多无状态组件越好,这说明逻辑处理都在上层,例如redux 中处理,这样可以在不渲染的前提下,测数据逻辑。

坏处:

  • 没有生命周期,没办法用shouldComponentUpdate阻止重新渲染,这也就是说,它没有帮助我们提高性能的作用,这也是它跟PureComponent最大的不同。

关于如何在实际中使用这两个组件,还要根据具体的实际情况来选择~

总结

综上可以看出,减少不必要的重新渲染对于提升我们的性能有很大的意义。我个人觉得,在实际中,用Perf跟why-did-you-update两个工具已经可以很好帮我们判断哪部分不需要重新渲染,我们可以根据结果做出优化。

遗留点

PureComponent那么好用,但是使用PureComponent是有条件的呀~

由于PureComponent只是做了一个浅比较,所以深层嵌套的对象跟数组都是比不出来的,可能会导致需要渲染的地方没有重新渲染的错误展示。

那么浅比较又是什么呢?下篇文章我们来继续探索

参考链接:

1、https://60devs.com/pure-component-in-react.html

2、https://engineering.musefind.com/how-to-benchmark-react-components-the-quick-and-dirty-guide-f595baf1014c

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏冷冷

jfinal自定义freemarker标签

jfinal自定义freemarker标签 ---- 1. config修改freemarkerrender public void after...

1726
来自专栏跟着阿笨一起玩NET

客户端程序传送图片到服务器

转载:http://www.cnblogs.com/networkcomms/p/4314898.html

552
来自专栏Angular&服务

Angular2 页面的生命周期

当被绑定的输入属性的值发生变化时调用,首次调用一定会发生在 ngOnInit之前。

1022
来自专栏编程思想之路

Android6.0源码分析之menu键弹出popupwindow菜单流程分析

例如上图,在按下菜单键后会弹出对应的菜单选项,准确来说,是在菜单键弹起后出现的一个popupwindow,那么从菜单键弹起到popupwindow创建所涉及到的...

1966
来自专栏恰同学骚年

ASP.Net WebForm温故知新学习笔记:一、aspx与服务器控件探秘

开篇:毫无疑问,ASP.Net WebForm是微软推出的一个跨时代的Web开发模式,它将WinForm开发模式的快捷便利的优点移植到了Web开发上,我们只要学...

502
来自专栏应用案例

Web前端面试题小集

来自:前端打小怪升级笔记,作者:spademan segmentfault.com/a/1190000008322096 一、一个页面上两个div左右铺满整个浏...

2649
来自专栏哲学驱动设计

WPF架构学习总结

预期读者     1. 初学者。     2. 懒得总结的人。:)     3. 想大致了解WPF框架主要类的功能的人。 前言     学习WPF也有段时间了,...

1868
来自专栏技术墨客

React学习(3)——列表、键值与表单 原

    例子中使用map方法将每个元素的值*2,最后得到的数组为:[2, 4, 6, 8, 10]。在React中,处理组件数组的方式与之类似。

813
来自专栏更流畅、简洁的软件开发方式

【自然框架】之表单控件(一)实体类(Class)VS 字典(Dictionary)

用一个具体一点的例子来说一下,我实现单表的添加、修改的思路和方式,顺便和三层里的实体类的方式做一下对比。 一、我的拆分思想之一       简单的操作和复杂的操...

2038
来自专栏MasiMaro 的技术博文

Windows程序设计笔记(二) 关于编写简单窗口程序中的几点疑惑

在编写窗口程序时主要是5个步骤,创建窗口类、注册窗口类、创建窗口、显示窗口、消息环的编写。对于这5个步骤为何要这样写,当初我不是太理解,学习到现在有些问题我基本...

733

扫码关注云+社区