ItIron2023
react
我們昨天看了一個不必要re-render造成的效能問題並利用React.memo來解決,到這邊一切相安無事,那麼昨天的文章也許會給你一種感覺。
我他媽還不用爆React.memo?
如果這類的想法有閃進你的腦袋,那不好意思今天我要戳破那個美好的想像泡泡了,馬上來看一個例子吧!
首先老樣子請你觀察這個codesandbox以及下方的gif檔案。
今天的情境是這樣的,自從學到了React.memo後,該名工程師就把所有的組件都套上這個HOC,寫出了以下的程式碼,頁面上的結構很單純,有著一個按鈕與一個大組件LargeList
,這個組件會接受來自大組件的props、也就是一個item list並渲染出許多個小組件SmallComponent
,原本預期這樣寫就可以解決當App因為state更新而造成LargeList
不必要的重新渲染,但正如gif所展示的情況,更新App的state仍舊造成LargeList
重新渲染並也理所當然地讓子組件SmallComponent
重新渲染,請試著解釋為什麼這個例子中React.memo並沒有效果。
const SmallComponent = memo(({ item }) => {
console.log("Rendering SmallComponent");
return <div>{item.name}</div>;
});
const LargeList = memo(({ items }) => {
console.log("Rendering LargeList");
return items.map((item, index) => <SmallComponent key={index} item={item} />);
});
export default function App() {
const [count, setCount] = useState(0);
const items = [{ name: "apple" }, { name: "banana" }, { name: "cherry" }];
return (
<div>
<h1>Optimization with React.memo the wrong way</h1>
<button onClick={() => setCount((prev) => prev + 1)}>
Increment Count: {count}
</button>
<LargeList items={items} />
</div>
);
}
接續昨天的進度,我們今天繼續來討論React.memo,若你真的有看昨天的文章並去補課,我想今天的題目對你來說再簡單也不過了,實際上這份程式碼犯的錯誤可不止在LargeList
中。而這類的情況在實務中層出不窮,React為了優化效能而有了許多的工具讓開發者使用,像是這兩天討論的React.memo,或是幾個常見的hooks例如useMemo、useCallback等,他們核心的目標都是為了避免不必要的重複操作,用得好的話自然沒什麼問題,但若是你並沒有妥善地使用,很多時候你都是在扯後腿罷了,因為比對props這樣的行為是需要成本的。
我們昨天有講過React.memo會先確認傳入的props是否有更新,若沒有更新的話就skip渲染的過程,而判斷是否有更新的方式則是淺比對(shallow comparison),也就是說如果是物件的話,它單純就是看該物件的reference是否仍是一樣,聽起來是不是有點熟悉,沒錯!我們在dependency array時也講過react是如何判斷更新的,當你想通之後一切其實都是相同的,今天的問題在於我們item的寫法
const items = [{ name: "apple" }, { name: "banana" }, { name: "cherry" }];
<LargeList items={items} />
每一次的重新渲染都會完整地跑過組件內的程式碼,這其中自然也包括函數與變數的宣告,因此雖然變數名都叫items,但實際上對react來說你是每一次都建立了一個全新reference的物件然後傳給LargeList
,因此每一次對LargeList
來說props都在變動,自然就不會跳過渲染,最終造成你看到的狀況。
想解決的方法也很簡單,假設你真的因為某種原因需要宣告這類固定值的陣列,大致上有兩種常見的手段
只要宣告在組件外,那麼他就不屬於渲染過程的一部分,自然也不會重新宣告,這麼一來reference就會保持相同讓React.memo如你所想的運作。
另一種做法我個人較為不推薦,但使用useMemo確實可以解決這個問題,原理有些類似,我們可以利用一個空的dependency array讓useMemo不會重新呼叫,那麼就一直可以保持相同的reference。
const items = useMemo(
() => [
{ name: "apple" },
{ name: "banana" },
{ name: "cherry" }
],
[] // Dependency array is empty, so items won't be recreated
);
再次強調我並不推薦在這個情況下使用useMemo,原因在於這次的陣列相當單純,並不是個非常吃效能的陣列,這就跟胡亂使用React.memo有著異曲同工之妙。
我們今天看了一個非常常見(是的,至少我看過不少實務上的例子)誤用React.memo的例子,沒搞清楚使用情境導致了react去做沒意義的比對,反而讓效能更差。你僅應該在以下兩種情況都滿足時去使用React.memo,其於絕大多數時間請你離他越遠越好。
1. 組件渲染的成本很高
2. 該組件會頻繁的重新渲染
追求效能優化並不是壞事,但不正確的手段只會將事情越搞越糟,在做任何的優化之前永遠記得去設benchmark,確保自己真的有提升你網站的效能,這是個非常深的坑,我自己也僅僅摸到皮毛而已!希望今天也讓你有所收穫,我們明天見吧!
本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!