使用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之后,它就能默认提供一些功能了。
你将在终端中看到类似下面的日志信息:
每当whyDidYouRender检测到有不必要的渲染时,它就会打印出这样的日志信息,我们甚至可以自定义日志的格式,或者做一些其它处理如数据上报。
whyDidYouRender会检查如下的情况,并发出警告:
- Different Props:各项Props没有变化,但是Props这个对象本身变化了。
- Different State: 各项State没有变化,但是State这个对象本身变化了。
- Props or State Equals by Value: Props或State确实变化了,但是它们序列化之后的值完全相同。
- Children / React Elements 因为children变化等导致的渲染
- React-Redux: 在Redux中执行不必要的更新(更新前后值相同)也会被检查出来
- 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默认用法(看日志)有不足之处
- 信息多且乱,难以定位。尤其是在为很多组件配置了whyDidYouRender静态变量之后
- 有误报的情况
- 不能检测出虽然看起来一切正常,但是实际上也可以优化的场景。如异步回调函数中循环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的次数,区分两个场景:
- 在React的生命周期方法和事件的处理函数(如onClick)中(不包含回调),多次串行setState只会触发一次render
- 对于其它情况,每次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。
为目标组件设置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中,查找其渲染的原因。
图中updateInfo
是一个对象,包含本次渲染的原因。
当它是undefined时,代表本次渲染是首次渲染。其它情况下,它的值一般形如:
reason字段中即为本次渲染的原因。
在上图的例子中,reason是props中的两个属性发生了变化,但是stringify后值完全相同。所以是deepEquals。
这就说明很可能这个组件的父组件中,存在不必要的state变化,或在父组件中渲染当前组件时,每次都重新生成了meta、config两个属性。
问题很可能出在父组件中,所以我们可以继续回溯调用堆栈,找到父组件的render函数,或本组件的setState等函数。
以上。