最近研究过一段时间前端渲染方面的性能优化,发现了有的组件虽然在页面中只有一个实例,但是在初始化时前前后后渲染了十多次!
大多数时候,这种问题也不大,因为有vdom和diff算法的存在,所以虽然有反复的render,但是当它遇到 trash layout的时候,问题就很大条了。
whyDidYouRender可以很好地定位此类问题,下面我介绍下它的用法。
npm install @welldone-software/why-did-you-render --save
['@babel/preset-react', {
runtime: 'automatic',
development: process.env.NODE_ENV === 'development',
importSource: '@welldone-software/why-did-you-render',
}]
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
然后在应用的最开始引入它:
import './wdyr'; // <--- first import
import 'react-hot-loader';
import {hot} from 'react-hot-loader/root';
import React from 'react';
import ReactDOM from 'react-dom';
// ...
import {App} from './app';
// ...
const HotApp = hot(App);
// ...
ReactDOM.render(<HotApp/>, document.getElementById('root'));
关于Hooks的特殊处理,以及一些概念的介绍,可以参考其官方文档。
最后再在希望检测的组件中,配置上静态的whyDidYouRender变量。
class BigList extends React.Component {
static whyDidYouRender = true
render(){
return (
//some heavy render you want to ensure doesn't happen if its not necessary
)
}
}
按上述方式安装和配置好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会认为其值每次都发生了变化
<ClassDemo
regEx={/something/}
fn={function something(){}}
fn2={this.foo.bind(this)}
date={new Date('6/29/2011 4:52:48 PM UTC')}
reactElement={<div>hi!</div>}
/>
wyDidYouRender默认用法(看日志)有不足之处
- 信息多且乱,难以定位。尤其是在为很多组件配置了whyDidYouRender静态变量之后
- 有误报的情况
- 不能检测出虽然看起来一切正常,但是实际上也可以优化的场景。如异步回调函数中循环setState
第一条不用解释,实际使用一下就能感觉出来了。日志会比较多,单看一条比较清晰,但是多了之后就比较乱。
有误报的情况,是指有的时候本来是正常更新,但是因为用法有问题,且是常见问题,导致其产生误报。
举个栗子:
handleChange = (val) => {
this.state.val = val
this.setState(this.state)
}
// 或
handleChange2 = (val) => {
const { someObj }= this.state
someObj.val = val
this.setState({ someObj })
}
这两个函数都会导致识报Different State
错误。原因是在whyDidYouRender判断的时候,prevState和newState实际上是同一个对象,whyDidYouRender认为它的值完全相同,却触发了setState,此setState操作是可以避免的。但是在这个例子中,其实是应该渲染的,不应当警告。而且即使说handleChange
是有问题的,不应当直接改state,事实上handleChange2
这种方式是很自然的,且一般不影响功能的,很难避免这种写法。
whyDidYouRender不能检测出来,但是需要优化的场景:
首先要说明下串行setState之后render的次数,区分两个场景:
- 在React的生命周期方法和事件的处理函数(如onClick)中(不包含回调),多次串行setState只会触发一次render
- 对于其它情况,每次setState都会触发render函数执行
那么如下的场景中,render执行次数比较多,肯定是需要优化的:
badSetState = (newState) => {
setTimeout(() => {
const keys = Object.keys(newState)
// 因为在异步回调函数中,所以每次setState都会触发渲染
keys.forEach(item => this.setState({ [item]: newState[item] }))
}, 0)
}
然而只要保证每次this.setState
时值真的不同,那么whyDidYouRender就检测不到。因为在它看来state确实发生了变化,是正常变更。
在实际的优化过程中,我发现将whyDidYouRender与Devtools中的Performance结合,再加上针对性地下断点,会起到很好的优化指导作用。
下面我分享下具体的做法:
打开Performance工具的录制功能,录制一断要诊断的性能记录。就会得到一个类似于上图的报表。
在这个报表中,随意选择一段范围,就可以看到这段时间内对性能消耗最多的点。
关于Performance工具的进一步介绍,可参考我之前的一篇博文:前端性能优化之读懂Performance。
找到可疑组件之后,为其设置whyDidYouRender和断点。
class BigList extends React.Component {
static whyDidYouRender = true
render(){
// 下断点在render函数中
return (
//some heavy render you want to ensure doesn't happen if its not necessary
)
}
}
如果使用的是第三方组件,不能修改其源码,也可以在外部给它赋值whyDidYouRender
属性,如:
import { Tabs } from 'antd'
Tabs.whyDidYouRender = true
在带有whyDidYouRender的render函数中下断点之后,可以通过堆栈回溯到whyDidYouRender中,查找其渲染的原因。
图中updateInfo
是一个对象,包含本次渲染的原因。
当它是undefined时,代表本次渲染是首次渲染。其它情况下,它的值一般形如:
reason字段中即为本次渲染的原因。
在上图的例子中,reason是props中的两个属性发生了变化,但是stringify后值完全相同。所以是deepEquals。
这就说明很可能这个组件的父组件中,存在不必要的state变化,或在父组件中渲染当前组件时,每次都重新生成了meta、config两个属性。
问题很可能出在父组件中,所以我们可以继续回溯调用堆栈,找到父组件的render函数,或本组件的setState等函数。
以上。