logo

鱼肚的博客

Don't Repeat Yourself

使用whyDidYouRender定位React中的重复渲染

最近研究过一段时间前端渲染方面的性能优化,发现了有的组件虽然在页面中只有一个实例,但是在初始化时前前后后渲染了十多次!

大多数时候,这种问题也不大,因为有vdom和diff算法的存在,所以虽然有反复的render,但是当它遇到 trash layout的时候,问题就很大条了。

whyDidYouRender可以很好地定位此类问题,下面我介绍下它的用法。

安装和配置whyDidYouRender

安装whyDidYouRender

1npm install @welldone-software/why-did-you-render --save

配置Babel

1['@babel/preset-react', {
2  runtime: 'automatic',
3  development: process.env.NODE_ENV === 'development',
4  importSource: '@welldone-software/why-did-you-render',
5}]

创建初始化配置文件wdyr.js

1import React from 'react';
2
3if (process.env.NODE_ENV === 'development') {
4  const whyDidYouRender = require('@welldone-software/why-did-you-render');
5  whyDidYouRender(React, {
6    trackAllPureComponents: true,
7  });
8}

然后在应用的最开始引入它:

1import './wdyr'; // <--- first import
2
3import 'react-hot-loader';
4import {hot} from 'react-hot-loader/root';
5
6import React from 'react';
7import ReactDOM from 'react-dom';
8// ...
9import {App} from './app';
10// ...
11const HotApp = hot(App);
12// ...
13ReactDOM.render(<HotApp/>, document.getElementById('root'));

关于Hooks的特殊处理,以及一些概念的介绍,可以参考其官方文档

最后再在希望检测的组件中,配置上静态的whyDidYouRender变量。

1class BigList extends React.Component {
2  static whyDidYouRender = true
3  render(){
4    return (
5      //some heavy render you want to ensure doesn't happen if its not necessary
6    )
7  }
8}

whyDidYouRender中默认提供的功能

按上述方式安装和配置好whyDidYouRender之后,它就能默认提供一些功能了。

你将在终端中看到类似下面的日志信息:

Image for post

每当whyDidYouRender检测到有不必要的渲染时,它就会打印出这样的日志信息,我们甚至可以自定义日志的格式,或者做一些其它处理如数据上报。

whyDidYouRender会检查如下的情况,并发出警告:

  1. Different Props:各项Props没有变化,但是Props这个对象本身变化了。
  2. Different State: 各项State没有变化,但是State这个对象本身变化了。
  3. Props or State Equals by Value: Props或State确实变化了,但是它们序列化之后的值完全相同。
  4. Children / React Elements 因为children变化等导致的渲染
  5. React-Redux: 在Redux中执行不必要的更新(更新前后值相同)也会被检查出来
  6. Equal Dates、Regular Expressions、React Components And Functions:一些看似是常量的东西,实际上是每次都会重新创建的,因其不是简单数据类型,而是Object,JS会认为其值每次都发生了变化
1<ClassDemo
2  regEx={/something/}
3  fn={function something(){}}
4  fn2={this.foo.bind(this)}
5  date={new Date('6/29/2011 4:52:48 PM UTC')}
6  reactElement={<div>hi!</div>}
7/>

whyDidYouRender中的不足与误报

wyDidYouRender默认用法(看日志)有不足之处

  1. 信息多且乱,难以定位。尤其是在为很多组件配置了whyDidYouRender静态变量之后
  2. 有误报的情况
  3. 不能检测出虽然看起来一切正常,但是实际上也可以优化的场景。如异步回调函数中循环setState

第一条不用解释,实际使用一下就能感觉出来了。日志会比较多,单看一条比较清晰,但是多了之后就比较乱。

有误报的情况,是指有的时候本来是正常更新,但是因为用法有问题,且是常见问题,导致其产生误报。

举个栗子:

1handleChange = (val) => {
2	this.state.val = val
3  this.setState(this.state)
4}
5
6// 或
7handleChange2 = (val) => {
8	const { someObj }= this.state
9  someObj.val = val
10  this.setState({ someObj })
11}

这两个函数都会导致识报Different State错误。原因是在whyDidYouRender判断的时候,prevState和newState实际上是同一个对象,whyDidYouRender认为它的值完全相同,却触发了setState,此setState操作是可以避免的。但是在这个例子中,其实是应该渲染的,不应当警告。而且即使说handleChange是有问题的,不应当直接改state,事实上handleChange2这种方式是很自然的,且一般不影响功能的,很难避免这种写法。

whyDidYouRender不能检测出来,但是需要优化的场景:

首先要说明下串行setState之后render的次数,区分两个场景:

  1. 在React的生命周期方法和事件的处理函数(如onClick)中(不包含回调),多次串行setState只会触发一次render
  2. 对于其它情况,每次setState都会触发render函数执行

那么如下的场景中,render执行次数比较多,肯定是需要优化的:

1badSetState = (newState) => {
2  setTimeout(() => {  
3    const keys = Object.keys(newState)
4    
5    // 因为在异步回调函数中,所以每次setState都会触发渲染
6    keys.forEach(item => this.setState({ [item]: newState[item] }))
7  }, 0)
8}

然而只要保证每次this.setState时值真的不同,那么whyDidYouRender就检测不到。因为在它看来state确实发生了变化,是正常变更。

whyDidYouRender的实际应用

在实际的优化过程中,我发现将whyDidYouRender与Devtools中的Performance结合,再加上针对性地下断点,会起到很好的优化指导作用。

下面我分享下具体的做法:

首先通过Performance工具确定性能阻塞点,寻找可疑组件

performance

打开Performance工具的录制功能,录制一断要诊断的性能记录。就会得到一个类似于上图的报表。

在这个报表中,随意选择一段范围,就可以看到这段时间内对性能消耗最多的点。

关于Performance工具的进一步介绍,可参考我之前的一篇博文:前端性能优化之读懂Performance

为目标组件设置whyDidYouRender和断点

找到可疑组件之后,为其设置whyDidYouRender和断点。

1class BigList extends React.Component {
2  static whyDidYouRender = true
3  render(){
4    // 下断点在render函数中
5    return (
6      //some heavy render you want to ensure doesn't happen if its not necessary
7    )
8  }
9}

如果使用的是第三方组件,不能修改其源码,也可以在外部给它赋值whyDidYouRender属性,如:

1import { Tabs } from 'antd'
2
3Tabs.whyDidYouRender = true

进入render函数中的断点,然后分析原因

在带有whyDidYouRender的render函数中下断点之后,可以通过堆栈回溯到whyDidYouRender中,查找其渲染的原因。

call stack

图中updateInfo是一个对象,包含本次渲染的原因。

当它是undefined时,代表本次渲染是首次渲染。其它情况下,它的值一般形如:

updateInfo

reason字段中即为本次渲染的原因。

在上图的例子中,reason是props中的两个属性发生了变化,但是stringify后值完全相同。所以是deepEquals

这就说明很可能这个组件的父组件中,存在不必要的state变化,或在父组件中渲染当前组件时,每次都重新生成了meta、config两个属性。

问题很可能出在父组件中,所以我们可以继续回溯调用堆栈,找到父组件的render函数,或本组件的setState等函数。

image-20210111212247294

以上。