React性能探索 — 避免不必要渲染

  • 2019 年 12 月 4 日
  • 筆記

本文作者:IMWeb 黃qiong 原文出處:IMWeb社區 未經同意,禁止轉載

背景

上一篇文章的結尾 http://imweb.io/topic/5985cc4d35d7d0a321c5eb75 我們說到,也許,不是所有的節點都需要重新渲染,對於那些不需要渲染的節點,我們如何找到它們並做優化呢?

本篇文章來具體解答這個問題。

應用分析

首先,先看這個應用:頁面的兩部分分別渲染5000個節點,從1-5000。當點擊按鈕之後,第二部分的節點會更新,重新渲染從2-5001的數字,但是第一部分保持不變。

import React, { createElement, Component } from 'react';  import {render} from 'react-dom';  import Perf from 'react-addons-perf';  import ListItem from './ListItem'    function arrayGenerator(length) {    return Array.apply(null, { length: length }).map(Number.call, Number)  }      class App extends Component {    constructor(props) {      super(props)      this.state = {        multiplier: 1      }    }      resetMultiplier() {      this.setState({ multiplier: 2 })    }      render() {      return (        <div className="App">          <button onClick={this.resetMultiplier.bind(this)}>Click Me</button>          <ul>            {              arrayGenerator(5000).map(i => {                return <ListItem key={i} text={i}/>              })            }            {              arrayGenerator(5000).map(i => {                return <ListItem key={i} text={i + this.state.multiplier}/>              })            }          </ul>        </div>      );    }  }    render(<App />,document.getElementById('main'));

gitbug 鏈接: https://github.com/hhhuangqiong/performance-for-react

感興趣的同學可以下載跑一跑程式碼

分析更新時間

這裡用react的Perf工具來測量重新渲染的時間。

使用方法:

npm install --save-dev react-addons-perf  import Perf from 'react-addons-perf'

這裡主要用到四個方法:

  • Perf.start():開始計時
  • Perf.stop():結束計時
  • Perf.printInclusive():列印組件總的渲染時間
  • Perf.printWasted():列印浪費的時間

開始計時的函數,我把它放到resetMultiplier里,即將發生發生改變時開始計時。然後在componentDidUpdate里,用Perf.stop()結束計時,然後列印渲染組件的時間跟浪費的時間。

  componentDidUpdate() {      Perf.stop()      Perf.printInclusive()      Perf.printWasted()    }      resetMultiplier() {      Perf.start()      this.setState({ multiplier: 2 })    }

當我們點擊按鈕,可以看到控制台列印出下面的資訊:

由控制台的數據可以看出,App用了90.59ms渲染,其中渲染ListItem的時間為55ms,渲染了10000次,其中有5000次是浪費的,因為這部分頁面的內容完全沒有更新的改動。

如何修復

既然是不需要渲染,那就要阻止它的渲染。React給我們提供了一個方法shouldComponentUpdate(),當這個方法返回true的時候,需要重新渲染,false的時候不需要(默認是true).

在這個栗子中,只要text的值不變,就不需要重新渲染。所以,可以這樣改寫ListItem 的shouldComponentUpdate

import React, { Component } from 'react'    export default class ListItem extends Component {    shouldComponentUpdate(nextProps, nextState) {      return nextProps.text !== this.props.text    }      render() {      let { text } = this.props      return <li>{text}</li>    }  }

在重新點擊一下按鈕,在控制台可以發現

App總的渲染時間降到了62.14ms,並且ListItem只重新渲染了5000個節點,完全消除了浪費的渲染。

對於上面的寫法,React提供了一個新的組件PureComponent來做這件事,它會自動淺對比props/state,當兩者相同的時候不渲染節點。所以,listItem又可以改寫成

import React, { PureComponent } from 'react'    export default class ListItem extends PureComponent {    render() {      let { text } = this.props      return <li>{text}</li>    }  }

跑一跑程式碼

通過控制台可以看到達到的效果是一樣的(有點誤差是正常的)。

這裡再安利一個可以發現應用里是否存在不該重新渲染的節點工具:why-did-you-update

使用方法

npm i --save-dev why-did-you-update  import React from 'react'  if (process.env.NODE_ENV !== 'production') {    const {whyDidYouUpdate} = require('why-did-you-update')    whyDidYouUpdate(React)  }

然後點擊按鈕看控制台

可以看到不應該重新渲染的節點出現了Value did not change. Avoidable re-render!的警告,是不是很實用!

注意的點

PureComponent只會淺比較,所以不適合用於深層嵌套的對象。同時,PureComponent不僅僅會跳過自己的重新渲染,還會跳過它所有子節點的,所以要注意,用它的時候是最好沒有子節點並且不依賴於global state的展示型組件。

與Staleless的關係

不知道有沒有人跟我有這樣的疑問,無狀態組件跟純凈組件有什麼不同?這裡做一個區分:

無狀態組件只是作為一個展示組件,它的好處是:

  • 易復用,易測試
  • 與邏輯處理數據層解耦,一般來說,app里有越多無狀態組件越好,這說明邏輯處理都在上層,例如redux 中處理,這樣可以在不渲染的前提下,測數據邏輯。

壞處:

  • 沒有生命周期,沒辦法用shouldComponentUpdate阻止重新渲染,這也就是說,它沒有幫助我們提高性能的作用,這也是它跟PureComponent最大的不同。

關於如何在實際中使用這兩個組件,還要根據具體的實際情況來選擇~

總結

綜上可以看出,減少不必要的重新渲染對於提升我們的性能有很大的意義。我個人覺得,在實際中,用Perf跟why-did-you-update兩個工具已經可以很好幫我們判斷哪部分不需要重新渲染,我們可以根據結果做出優化。

遺留點

PureComponent那麼好用,但是使用PureComponent是有條件的呀~

由於PureComponent只是做了一個淺比較,所以深層嵌套的對象跟數組都是比不出來的,可能會導致需要渲染的地方沒有重新渲染的錯誤展示。

那麼淺比較又是什麼呢?下篇文章我們來繼續探索

參考鏈接:

1、https://60devs.com/pure-component-in-react.html