日常React开发中,可能不少人都忽略了性能问题 —— 糙,快,猛 搞定 feature 就是了
日积月累的,性能问题就会慢慢浮现出来,比如页面打开缓慢,动画迟滞,路由跳转缓慢等等。
React性能问题中有多种类型,本文即将探讨的是多余的组件渲染问题 —— 即组件出现不必要渲染的情况。
多余的组件渲染 场景一
看个虚构的例子:
BigListView.js
import React from 'react';
// 函数式组件(什么是函数组件? https://reactjs.org/docs/components-and-props.html#function-and-class-components )
const BigListView = (props) => {
const {items} = props;
console.log("BigListView render on " + Date.now());
return (
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
);
};
export default BigListView;
这虽然是个简单的列表视图组件,在本文中打算用来代表现实开发中的复杂列表视图(比如 taobao商品列表)—— 一旦多余渲染可能导致页面卡顿。
第五行代码,每次渲染时,都会打印一条 log (后面跟上当前时间戳),如
![3e262cec64f3022876fc9d707e74701d.png](https://i-blog.csdnimg.cn/blog_migrate/759d62caa388c45e8e6f36cc550f4947.jpeg)
这个组件被一个外部另一个页面组件 MyApp 使用
MyApp.js
import React, {useState} from 'react';
import BigListView from './BigListView';
// 虚构的列表数据
const initItems = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const MyApp = () => {
const [items, setItems] = useState(initItems);
const onUpdateList = () => {
setItems([...items, Math.random() * 100]) // 每次增加一个随机的列表数据
};
const [counter, setCounter] = useState(0);
const onUpdateCounter = () => {
setCounter(counter + 1) // 给计数器加 1
};
return (
<div>
<button onClick={onUpdateCounter}>Update Counter</button>
<div>{counter}</div>
<br/>
<button onClick={onUpdateList}>Update List</button>
<BigListView items={items}/>
</div>
);
};
export default MyApp;
在这个虚构的页面中,有个计数器 和 上述的列表视图,如下图
![373c841052b3177cf3c8534e512b1390.png](https://i-blog.csdnimg.cn/blog_migrate/03d22512d527b861357ee82d56488718.png)
当我们点击 Update List 按钮的时候,列表就会新增一个数据,当然console里也会新加一条log,这是我们预期的表现。
当我们点击 Update Counter 按钮的时候,计数器数值加 1 。但是,我们发现 console里还会新加一条 BigListView 的 log,说明 BigListView 也被重新渲染了一次 —— 我们明明只是更改了counter 的值,items 值没变,为什么会触发 BigListView 的重新渲染?
(* 本文先不探讨DOM是否会真的重新渲染以及React 的 virtual dom diff 算法)
即我们预期的是
只当 BigListView 的 props 改变的时候,才应该触发 BigListView 的重新渲染
这是性能调优的第一步:对类组件我们可以使用 React.PureComponent ,对函数组件我们可以使用 React.memo 来保证仅当props(或state)改变时才会触发重新渲染
更新后的 BigListView.js 如下
import React from 'react';
const BigListView = (props) => {
const {items} = props;
console.log("BigListView render on " + Date.now());
return (
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
);
};
export default React.memo(BigListView); // 使用React.memo优化
现在,点击 Update counter 就不会导致 BigListView 重新渲染了
多余的组件渲染 场景二
接着来了个新需求,BigListView 会在多个页面被复用,但是在不同页面的样式要不一样。
于是我们在 BigListView 的 props 新增一个 style 属性方便父组件传入样式对象
import React from 'react';
const BigListView = (props) => {
const {items, style} = props;
console.log("BigListView render on " + Date.now());
return (
<ul style={{...style}}> // 允许外部传入样式
{items.map(item => <li key={item}>{item}</li>)}
</ul>
);
};
export default React.memo(BigListView);
在 MyApp.js 中我们希望这个列表宽度可以占满界面,于是传入{width: '100%'} ,更改如下
import React, {useState} from 'react';
import BigListView from './BigListView';
const initItems = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const MyApp = () => {
const [items, setItems] = useState(initItems);
const onUpdateList = () => {
setItems([...items, Math.random() * 100])
};
const [counter, setCounter] = useState(0);
const onUpdateCounter = () => {
setCounter(counter + 1)
};
return (
<div>
<button onClick={onUpdateCounter}>Update Counter</button>
<div>{counter}</div>
<br/>
<button onClick={onUpdateList}>Update List</button>
<BigListView items={items} style={{width: '100%'}}/> // style对象
</div>
);
};
export default MyApp;
更改完之后,发现老问题又出现了——点击 Update Counter 又触发了 BigListView 的重新渲染。
![20906b610462149c11d878b60f378c31.png](https://i-blog.csdnimg.cn/blog_migrate/17c4dfe63ad282d54275efbf91b8362b.jpeg)
在继续往下看之前,你可以先思考一下原因~
公布答案:
如果你是 JavaScript 老司机,可能一眼就看出来了 —— 因为 MyApp.js 都给 BigListView 传入style 属性是个临时的对象字面量,每次 MyApp 重新渲染都会创建一个全新的 对象字面量。因此对 BigListView 而言每次传入的 props 都不同。
解决方案有两种:
- (推荐)MyApp.js 中把 {width: '100%'} 定义成一个 常量,所以每次传给 BigListView 的都是同一个对象
- 对 BigListView,使用定制化的 React.memo (即传入第二个参数) 精确判断哪些 props 改变才触发重新渲染,缺点是以后每次增加 props 都需要检查这个判断逻辑 ,维护起来比较麻烦(这个方案的代码就不写了,参考官方文档 )
采取方案一后的 MyApp.js 代码
import React, {useState} from 'react';
import BigListView from './BigListView';
const initItems = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const listViewStyle = {width: '100%'}; // 定义成常量
const MyApp = () => {
const [items, setItems] = useState(initItems);
const onUpdateList = () => {
setItems([...items, Math.random() * 100])
};
const [counter, setCounter] = useState(0);
const onUpdateCounter = () => {
setCounter(counter + 1)
};
return (
<div>
<button onClick={onUpdateCounter}>Update Counter</button>
<div>{counter}</div>
<br/>
<button onClick={onUpdateList}>Update List</button>
<BigListView items={items} style={listViewStyle}/> // 只引用常量
</div>
);
};
export default MyApp;
如何快速定位问题
上述的例子中,我们成功优化了代码,避免 BigListView 的多余重复渲染。这对于经验丰富的React开发而言或许并不困难,但是新手可能兜兜转转半天都看不出端倪。
如何轻松地便利地快速定位这类问题呢?
主角终于出场:
Why Did You Render (Github: https://github.com/welldone-software/why-did-you-render )
(3527个star,只有10个open issue,上次更新是 14天前,又是个非常优秀的第三方库)
以下简称 wdyr
这个库可以代替上面 BigListView.js 中的这行代码
console.log("BigListView render on " + Date.now());
且更加强大 —— 根据配置可以具体罗列出导致重新渲染的的原因如 props 的哪个属性变化了,或者哪个hook变化了,哪个state变化了等等
例:在场景二中是用wdyr,可以看到如下的log
![a63d4005a144f6f2de41f92ef8a97f81.png](https://i-blog.csdnimg.cn/blog_migrate/b8afa315374ec52195a750024697f08f.jpeg)
信息非常的详细,清晰,甚至还有警告信息 —— ”different objects that are equal by value“前后的style对象的虽然不同但是值完全相等。
安装步骤
npm install @welldone-software/why-did-you-render --save
创建一个wdyr.js文件
import React from 'react';
if (process.env.NODE_ENV === 'development') { // 仅在本地开发中启用
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
在项目中的 index.js 第一行就 import 这个文件
使用
按上面的配置,默认情况下,
- wdyr 只会对PureComponents 或者 React.memo封装过的函数组件生效。所以,上面场景一中的 BigListView 是不会打印 log 。
- wdyr 只会对 props的异常变化(如内容完全相同)才打印 log
但是日常开发中,我倾向于 针对某些组件研究所有导致它们重新渲染的原因(包括正常的和异常的),再进一步优化代码 如 提升一些数据状态到父组件,赋值常量等等。
所以在日常开发中,我会在类似如下单独配置 BigListView.js (类组件的配置我就不写了,可以看官方文档)
import React from 'react';
const BigListView = (props) => {
const {items, style} = props;
return (
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
);
};
// 打印出所有导致 BigListView 渲染的原因
BigListView.whyDidYouRender = {
logOnDifferentValues: true
};
export default BigListView;
这种配置 显式地告诉wdyr 打印出导致 BigListView 渲染的所有原因
你可以随意尝试各种情况,看看wdyr可以打印出哪些类型的信息,这里我再分享几个截图
![296bb7931ac68aa5388cc8cf41afd3b5.png](https://i-blog.csdnimg.cn/blog_migrate/7744a0177a1b7643e12486bf010d2e4d.jpeg)
![0e219bfee47ea416abcecbfaf7975d73.png](https://i-blog.csdnimg.cn/blog_migrate/a1dd4e3df446fba4f58b0ad045c6b056.jpeg)
一旦原因确定,解决方案也相对容易想出来了。
个人使用体验
我已经在自己的项目中使用过一段时间了 wdyr ,总体体验是很不错的。
它除了帮助解决了一些明显的页面性能问题;在写新组件写新页面中,它也会不断提醒我哪些地方出现了 不必要的渲染 或者 看似合法却反复出现的渲染,让我进一步优化我的代码。
有兴趣的读者可以马上去阅读详细的官方文档,然后上手试试wdyr吧!
觉得不错的话,可以给我留言说说你们的体验。
![6a116f43204e4b760abfa96af6ccd39c.png](https://i-blog.csdnimg.cn/blog_migrate/1d0e2dcb06021c63e12dd204209919df.jpeg)
参考链接:
https://zh-hans.reactjs.org/docs/react-api.html#reactpurecomponent
https://zh-hans.reactjs.org/docs/react-api.html#reactmemo
https://github.com/welldone-software/why-did-you-render