js重新渲染div_React性能问题之多余的组件渲染及检查工具

日常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

这个组件被一个外部另一个页面组件 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

当我们点击 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

在继续往下看之前,你可以先思考一下原因~

公布答案:

如果你是 JavaScript 老司机,可能一眼就看出来了 —— 因为 MyApp.js 都给 BigListView 传入style 属性是个临时的对象字面量,每次 MyApp 重新渲染都会创建一个全新的 对象字面量。因此对 BigListView 而言每次传入的 props 都不同。

解决方案有两种:

  1. 推荐)MyApp.js 中把 {width: '100%'} 定义成一个 常量,所以每次传给 BigListView 的都是同一个对象
  2. 对 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

信息非常的详细,清晰,甚至还有警告信息 —— ”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 这个文件

使用

按上面的配置,默认情况下,

  1. wdyr 只会对PureComponents 或者 React.memo封装过的函数组件生效。所以,上面场景一中的 BigListView 是不会打印 log 。
  2. 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

0e219bfee47ea416abcecbfaf7975d73.png

一旦原因确定,解决方案也相对容易想出来了。


个人使用体验

我已经在自己的项目中使用过一段时间了 wdyr ,总体体验是很不错的。

它除了帮助解决了一些明显的页面性能问题;在写新组件写新页面中,它也会不断提醒我哪些地方出现了 不必要的渲染 或者 看似合法却反复出现的渲染,让我进一步优化我的代码。

有兴趣的读者可以马上去阅读详细的官方文档,然后上手试试wdyr吧!

觉得不错的话,可以给我留言说说你们的体验。

6a116f43204e4b760abfa96af6ccd39c.png

参考链接:

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值