React memo
在前端效能優化的過程中,大家通常第一時間想到的就是 memo 和 useMemo ,下面會介紹我們該在什麼時候用 memo 來包我們的 component,React.memo 背後的底層邏輯是什麼,以及它在什麼情況下有可能會失效。
基礎應用
假設今天我們有一個 Parent component,裡面有兩個 input bar 分別可以輸入 name 和 address,用戶輸入之後會透過 setState 改變他們的值,我們把其中一個值 name 傳到他的 Child component
Without memo
//
import { memo, useState } from 'react';
export default function Parent() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
Name{': '}
<input value={name} onChange={e => setName(e.target.value)} />
</label>
<label>
Address{': '}
<input value={address} onChange={e => setAddress(e.target.value)} />
</label>
<Child name={name} />
</>
);
}
const Child = function Child({ name }) {
console.log("Child was rendered at", new Date().toLocaleTimeString());
return <h3>Hello{name && ', '}{name}!</h3>;
};
當用戶更新 address 的時候, Parent 呼叫 setState 導致 re-render ,而此時由於 Parent component 的 re-render 行為,導致 Child 也跟著 re-render
With memo
const Child = memo(function Child({ name }) {
console.log("Child was rendered at", new Date().toLocaleTimeString());
return <h3>Hello{name && ', '}{name}!</h3>;
});
當我們把 Child 包上 memo 之後,就算Parent component 觸發 re-render,他也會根據傳進來的 name 跟之前一不一樣來決定需不需要 re-render。
即使 re-render不必然會導致元件的 reflow 和 repaint,但 memo 確實幫我們節省了 component函式執行以及 Virtual DOM 的比對成本。
底層原理
-
定義物件
當我們把 component 傳入
memo之後,React會建立一個帶有REACT_MEMO_TYPE的物件export function memo(type, compare) { const elementType = { $$typeof: REACT_MEMO_TYPE, type, compare: compare === undefined ? null : compare, }; return elementType; } -
建立 Fiber
接著當 React 要將 JSX element 建立為 Fiber 時,會去檢查他的 type,把 Fiber 標記為
MemoComponentdefault: { // $FlowFixMe[invalid-compare] if (typeof resolvedType === 'object' && resolvedType !== null) { switch (resolvedType.$$typeof) { // $FlowFixMe[invalid-compare] case REACT_MEMO_TYPE: fiberTag = MemoComponent; break getTag;MemoComponent在 update 時主要會執行以下幾件事:const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes); if (!hasScheduledUpdateOrContext) { const prevProps = currentChild.memoizedProps; let compare = Component.compare; compare = compare !== null ? compare : shallowEqual; if ( compare(prevProps, nextProps) && current.ref === workInProgress.ref ) { return bailoutOnAlreadyFinishedWork(...); } } // 不能 bailout,建立/取得 inner child 的 work-in-progress Fiber const newChild = createWorkInProgress(currentChild, nextProps); return newChild;簡單來說就是,要成功跳過 component,至少需要:
- 沒有目前優先級需要處理的 scheduled update。
- 它依賴的 context 沒改變。
- 舊 props 與新 props 比較相等。
ref沒改變。
只要其中一項不成立,就繼續處理 inner component。
-
比較差異
當我們沒有傳入 custom compare function的時候,React會預設使用
shallowEqualshallowEqual(prevProps, nextProps)shallowEqual會透過Object.is()比較每一個prop,而不是深層比較,他比較的是 prop 的 reference,所以當今天我們傳進來的 prop 是一個function或是一個object的話,對他來說每次的render都還是一個新的 reference,這件事情就有可能會導致React.memo無法達到我們想要的避免 re-render 的效果
總結
- 當我們不希望 Parent component 的 re-render 會導致 Child component re-render 的時候,在 Child component 包
memo - 當傳入的 props 是 function 或是 object 的時候,
React.memo就有可能會失效 React.memo不是萬靈丹,請把他當成優化的手段而非程式碼執行的必要
References
https://react.dev/reference/react/memo https://github.com/react/react/blob/main/packages/react/src/ReactMemo.js https://github.com/react/react/blob/main/packages/react-reconciler/src/ReactFiber.js https://github.com/react/react/blob/main/packages/react-reconciler/src/ReactFiberBeginWork.js