iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 19
1
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 19

【Day.19】React效能 - 用memo避免不必要的重複渲染

這一篇要討論的是function component的效能問題

在上一篇中,我們發現即使MenuItem接收的props並沒有被改變,MenuItem的return元素也和context無關,但是它在Menu的button按下時還是被重新渲染了。

  • src/component/MenuItem.js
import React, {useContext} from 'react';
import { OpenContext } from '../context/ControlContext';

const menuItemStyle = {
    marginBottom: "7px",
    paddingLeft: "26px",
    listStyle: "none"
};


function MenuItem(props){
    const isOpenUtil = useContext(OpenContext);
    return <li style={menuItemStyle}>{props.text}</li>;
}

export default MenuItem;
  • src/component/Menu.js
function Menu(props){
    const isOpenUtil = useContext(OpenContext);
    return (
        <div style={menuContainerStyle}>
            <p style={menuTitleStyle}>{props.title}</p>
            <button style={menuBtnStyle} onClick={
                ()=>{isOpenUtil.setOpenContext(!isOpenUtil.openContext)}
            }>
                {(isOpenUtil.openContext)?"^":"V"}
            </button>
            <ul>{props.children}</ul>
        </div>
    );
}

  • src/page/MenuPage.js
import React, {useState} from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];


const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);
    
    /* 請注意這一行的位置 */
    let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);
    
    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
        </OpenContext.Provider>
    );
}

export default MenuPage;

在上一篇的最後,我們雖然是這樣說的:

會造成這個問題是因為useContext原始的實作方法,不是去檢查該context更動會不會改變子元件。而是當context被更新時,直接讓所有使用useContext的元件都被重新渲染。

然而如果你把引入useContext的那行註解掉,會發現元件依然有被重複渲染。

import React from 'react';
//import { OpenContext } from '../context/ControlContext';

const menuItemStyle = {
    marginBottom: "7px",
    paddingLeft: "26px",
    listStyle: "none"
};


function MenuItem(props){
    //const isOpenUtil = useContext(OpenContext);
    return <li style={menuItemStyle}>{props.text}</li>;
}

export default MenuItem;

function component的效能問題

請注意MenuPage使用MenuItem的這一行

let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);

如果你把這一行寫在const MenuPage = () =>{}裡面,即使你沒有引入Context也會有重複渲染的問題

  • src/page/MenuPage.js
import React, {useState,useCallback} from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];

const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);
    let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);
    
    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
        </OpenContext.Provider>
    );
}

export default MenuPage;

這是因為我們前面說過,React的function component在每次重新渲染時都會呼叫整個Component函式的定義域。如果你把wording加工成MenuItem的map寫在MenuPage中,當MenuPage被重新渲染時,就會重新呼叫一次這個「把wording加工成MenuItem的map」

現在我們只是自己開發所有程式,可以透過把「加工元件的函式」拉到function component外解決,但在與別人合作時,就無法保證同事會怎麼寫。此時就要思考一件事:

「有的時候我們只負責製作元件,並沒有辦法確認使用這個元件的人是不是在函式定義域內加工元件,這個時候該怎麼辦才能確保不會有效能問題呢?」

換句話說,我們現在需要的就是一個「會幫我們檢查需不需要改變子元件」的中介層。

HOC(High Order Component)

High Order Component並不是一個元件的類別,它是一種特別的設計觀念:

「 用來加工 Component 的 function 」

意思是說,這類的函式的作用就是讓你把Component丟進去給它,它會幫你加工成一個新的Component。

const ComponentNew = HOCFunction(ComponentOld);

React.memo

memo就是React提供的「會幫我們檢查元件需不需要重新渲染」的中介層的一個HOC。

memo產生出的新元件會記憶住上一次元件的props值,當父元件被重新渲染,子元件沒有變動、但父元件又想要渲染它時,memo會去比較該子元件的props有沒有和前一次記憶的結果不同,如果有才重新渲染該子元件。

現在,讓我們從React函式庫中引入memo

  • src/component/MenuItem.js
import React, {memo} from 'react';

並在檔案的最後,讓export出去的Component用memo()包起來

import React, {memo} from 'react';

const menuItemStyle = {
    marginBottom: "7px",
    paddingLeft: "26px",
    listStyle: "none"
};


function MenuItem(props){
    return <li style={menuItemStyle}>{props.text}</li>;
}

export default memo(MenuItem);

現在我們再次用React-dev-tool去檢查我們的製作的元件:

就會發現MenuItem的確沒有被渲染了。

不要把memo當作效能的保證手段

雖然這個問題可以透過memo解決,但是請不要把memo當萬靈丹。因為你加了一層memo,React就要多做一件事情。當遇到像這個case有其他解決方法時(移到function component外去加工元件),我們就不應該讓程式多一個負擔。

那什麼時候才能加入useContext呢

我們會用大約四到五篇先來討論如何解決function component本身的效能問題,再回頭討論該如何處理useContext本身的效能問題。


上一篇
【Day.18】開發者工具React Dev tool與useContext的效能問題
下一篇
【Day.20】React效能 - 用useCallback避免函式的重新定義
系列文
從比入門再往前一點開始,一直到深入React.js30

尚未有邦友留言

立即登入留言