[Note] React: Performance & Patterns


Posted by urlun0404 on 2023-03-02

最近都在看這位開發者的文章、她寫的文章都很深入又淺顯易懂,這篇主要是彙整跟React效能有關的文章筆記,以及一些避免降低效能的模式(pattern),如果之後有看到其他的應該會陸續補上。


Re-render 重新渲染

作者提到大部分的React performance議題大概都跟「重新渲染(re-render)」有關,而React元件(component)重新渲染的時機如下:

  1. 自己的state、props或context改變會導致重新渲染
  2. 父(母)元件重新渲染,子元件沒做任何措施的話一定會跟著重新渲染

另外,React是採用嚴格比較(strict comparison)來判斷前次和這次渲染的變數是否相同,但React的元件本質上是函式,可以用JavaScript的觀點想成每次重新呼叫函式,函式內的區域變數(local variables)都會重新建立,尤其是物件(object) ─ 物件會要一塊新的記憶體儲存看起來相同的內容;同理,元件每次重新渲染,元件內的變數都會重新建立,而且要注意物件雖然看起來跟前一次渲染一樣,但其記憶體位址不同,在嚴格比較下就和前一次渲染的物件不同。

Anti-Pattern

歸納這篇文章提到的幾個不好的設計如下:

  • 把一個元件定義在另一個元件內 ─ 這會導致裡面的元件會被重新建立並且重新掛載(re-mount)到DOM(因為元件其實是函式物件!)

    function Parent(){
      function Child(){
          return <div>child</div>
      }
    
      return <Child />
    }
    
  • 使用useContext的value是物件(object):如下 Provider 的value其實是物件 {context}

    function Parent(){
    
      return <Context.Provider value={{context}}>
                  <button onClick={()=>console.log('重新渲染')}>重新渲染</button>
          </Context.Provider>
    }
    
  • 把子元件的props用 useMemouseCallback 包起來 ─ 若子元件的父(母)元件重新渲染,即使子元件的props相同,仍舊會跟著父(母)元件重新渲染:如下圖<Child/>(e)=>setChildState若直接寫在onClick={}會因為每次都會建立新的函式物件導致<Child/>本身重新渲染,所以範例用 useCallback把函式包起來;不過若是 parentState 改變導致 <Parent /> 重新渲染,<Child/>handleChildClick雖然沒有改變但仍舊會跟著 <Parent /> 一起重新渲染。

    function Parent(){
      const [parentState, setParentState] = useState([]);
      const [childState, setChildState] = useState([]);
    
      const handleChildClick = useCallback((e)=>console.log(e), []);
    
      return <Child childState={childState} onClick={handleChildClick}>
    }
    


有關前面提到的不良設計有幾個解決方法:

  1. 元件定義在其他元件以外;

     function Child(){
         return <div>child</div>
     }
    
     function Parent(){
         return <Child />
     }
    
  2. 物件型態的state、props或context用 useMemo 包起來;

     function Parent(){
         const memoContext = useMemo(() => {context}, [])
    
         return 
             <Context.Provider value={memoContext}>
                 <button onClick={()=>console.log('重新渲染')}>重新渲染</button>
             </Context.Provider>
     }
    
  3. 子元件用 useMemo 包起來。

     function Parent(){
         const [parentState, setParentState] = useState([]);
         const [childState, setChildState] = useState([]);
    
         const child = useMemo(
             ()=> <Child childState={childState} onClick={(e)=>console.log(e)}>
         , [childState]);
    
         return <>{child}</>
     }
    



但其實除了以上方法也有其他避免父(母)元件重新渲染導致子元件重新渲染的方法,例如:

  1. 使用children

     function Provider(){
         return <Context.Provider value={{context}}>
             {children}
         </Context.Provider>
     }
    
     function Parent(){
         return <Provider>
                     <button onClick={()=>console.log('重新渲染')}>重新渲染</button>
                 </Provider>
     }
    
  2. 把子元件用 React.memo 包起來

     function Parent(){
         const [parentState, setParentState] = useState([]);
         const [childState, setChildState] = useState([]);
    
         const handleChildClick = useCallback((e)=>setChildState, []);
    
         return React.memo(<Child childState={childState} onClick={handleChildClick}>);
     }
    



References
How to write performant React code: rules, patterns, do's and don'ts
Before You memo()


#React #performance #pattern







Related Posts

[雷雷夥伴] How to use azure function to manage azure resource

[雷雷夥伴] How to use azure function to manage azure resource

簡明約耳趣談軟體(Joel on Software)導讀書摘

簡明約耳趣談軟體(Joel on Software)導讀書摘

[week 1] 版本控制 - Git 概念 & 基本指令

[week 1] 版本控制 - Git 概念 & 基本指令


Comments