# [筆記] React 效能優化 # React 的 render 原理 react 具有 virtual DOM 機制,來處理 CSR 因為資料變更需要頻繁操作 DOM 而導致瀏覽器需要重複 reflow&repaint 的問題,這個過程相當耗費計算資源。 virtual DOM 是 React 用來記錄 DOM tree 上有那些元素、元素分別為什麼狀態的 Object,當 state 改變, React 會透過 diff 演算法,把新舊 virtual DOM 互相比較、篩選出有更動的部分,針對那個部分去進行DOM修改。 所以觸發流程會是長這樣: ```mermaid graph LR C[Component] --React--> Vir[Virtual DOM] --diff--> Ch[Change part]--ReactDOM--> D[DOM] ``` 其中最耗效能的部位會在: 1. diff 演算法 2. ReactDOM 修改 DOM 由此可知,若要優化效能,我們需要盡量避免: 1. **觸發元件 re-render** → 避免觸發 diff 演算法 2. **盡量保持 virtual DOM 的一致** → 避免觸發 ReactDOM 修改 DOM # **React 是如何決定 re-render 元件的** 其實比想像中簡單,react 預設就是: > **當 state 被更動或接受到新的 props,就會觸發 re-render 元件** > 但這個簡單的原理,碰上 JavaScript 的特性,就不這麼簡單了。 我們都知道,JavaScript 有 `by value` 和 `by reference` 的資料型別差異,前者稱為 **Primitive type (原始型別)**,後者稱作 **Object Type(物件型別)/reference Type(參考型別)**。 React Component 會針對 state 內的值去做嚴格相等(`===`),判斷是否需要 re-render。 當 state 是**原始型別**時,因為比較的是值,所以值不相等才會觸發 re-render。 而若是 object 這類型的**參考型別**,比較的則是 reference,也就是該物件在記憶體中的地址,但由於每一次創建 object 時,都會給定一個新的地址,基本上只要有新的 object 傳入 `setState`,不論裡面的值是否跟上次相同,都會觸發 re-render。 ```jsx //原始型別狀況 const oldState = 1; const newState = 1; oldState === newState; //true //參考型別狀況 const oldState = {id: 1}; const newState = {id: 1}; oldState === newState; //false ``` 而為了要觸發 `setState`,我們必須傳入新物件,才可以觸發 `state` 更新,在這種狀況下,無論你的 object 值是否相同,都會因為 reference 改變而觸發 re-render。 而當父層元件 re-render ,子層元件因為是父層元件的一部份,所以即便子層的 props 沒有變動,也一樣會 re-render,例如下面的例子: ```jsx const Content = () => <div>I'm Content!</div> const App = () => { const [state, setState] = useState() const onClick = () => setState(!state) return ( <> <button onClick={onClick}>change!</button> <Content /> {/* Content 沒有 props 但也一樣會 rerender*/} </> ) } ``` # React.memo、useMemo、useCallback 理解了 React 的 render 機制後,React 也提供了幾種方法來避免多餘且昂貴的 re-render。 下面是一個常見的 React 範例,由 App 包裹多個元件,並在這邊控制 state,將 state 傳入子元件的 props,請問: > 按下 `button` 後,更新 `otherState`,`Table` 是否會被 re-render? [^ReactQuiz] > ```jsx const Row = ({item, style}) => ( <tr style={style}> <td>{item.name}</td> </tr> ); const Table = ({list}) => { const itemStyle = { color: 'red' }; return ( <table> {list.map(item => <Row key={item.name} item={item} style={itemStyle} />)} </table> ) }; const App = () => { const defaultList = Array(10000).fill(0).map((val, index) => ({name: index})); const [list, setList] = useState(defaultList); const [otherState, setOtherState] = useState(0); const onClick = () => { setOtherState(1) }; return ( <> <Table list={list} /> <button onClick={onClick}>change state!</button> </> ) }; ``` - 答案是: 會!即便 Table 沒有傳入任何 props ,Table 也會因為 App re-render 而導致 re-render。 ## React.memo 上述的 `Table` 例子,照理來說因為 props 沒有變化,所以不需要 re-render,React 提供了 `React.memo` 這個方法,來避免這種情形。 `React.memo` 類似 class component 的 pure component,此方法會回傳一個 higher order component,會比較傳進來的 props 是否跟前次呼叫時相同,比較方式則採取淺拷貝(shallow copy)。 使用 React.memo 改寫過後會長這樣:[^ReactQuiz] ```jsx const Row = ({item, style}) => ( <tr style={style}> <td>{item.name}</td> </tr> ); const Table = React.memo(({list}) => { const itemStyle = { color: 'red' }; return ( <table> {list.map(item => <Row key={item.name} item={item} style={itemStyle} />)} </table> ) }); const App = () => { const defaultList = Array(10000).fill(0).map((val, index) => ({name: index})); const [list, setList] = useState(defaultList); const [otherState, setOtherState] = useState(0); const onClick = () => { setOtherState(1) }; return ( <> <Table list={list} /> <button onClick={onClick}>change state!</button> </> ) }; ``` 這樣就可以阻止 `Table` 被 re-render了。 那如果換成下面這個例子,點擊 `button` 後的確會改變 `list` 狀態,此時因為 `list` 的確發生變化,要繼續優化的話,我們需要把 `React.memo` 換成包裹住 `Row`。 下一個問題來了:[^ReactQuiz] > 當 `Table` re-render 時,使用 `React.memo` 包裹的 `Row` 是否達到優化效果? > A. 是,有 `React.memo` 的 `Row` 比 沒有的 `Row` 效能更好 B. 否,`React.memo` 包裹的 `Row` 和 沒有的 `Row` 效能差不多 C. 否,`React.memo` 包裹的 `Row` 比起沒有的 `Row` 效能更差 ```jsx const Row = React.memo(({item, style}) => ( <tr style={style}> <td>{item.name}</td> </tr> )); const Table = ({list}) => { const itemStyle = { color: 'red' }; return ( <table> {list.map(item => <Row key={item.name} item={item} style={itemStyle} />)} </table> ) }; const defaultList = Array(10000).fill(0).map((val, index) => ({name: index})); const App = () => { const [list, setList] = useState(defaultList); const [otherState, setOtherState] = useState(0); const onClick = () => { setList(prev => [...prev, {name: 123}]) }; return ( <> <Table list={list} /> <button onClick={onClick}>change state!</button> </> ) }; ``` 答案是:C,但你知道為什麼嗎? ### 問題出在 item ? 上面的例子,我們確定 `Table` 必定會 re-render,因為 list 添加了一個值,上面使用了解構賦值,所以我們知道,`list` 的 reference 必定會改變,那 `list` 中的 `item` 也是一個 object,`item` 的 reference 也會改變嗎? 答案是:不會!因為解構賦值本身是淺拷貝,第二層的 object reference 並不會改變。 所以可以確定這邊的 `item` 跟 `item.name` 都不會變動,那問題會出在哪呢? ```jsx const Table = ({list}) => { const itemStyle = { color: 'red' }; return ( <table> {list.map(item => <Row key={item.name} item={item} style={itemStyle} />)} </table> ) }; const App = () => { const defaultList = Array(10000).fill(0).map((val, index) => ({name: index})); const [list, setList] = useState(defaultList); const [otherState, setOtherState] = useState(0); const onClick = () => { setList(prev => [...prev, {name: 123}]) }; return ( <> <Table list={list} /> <button onClick={onClick}>change state!</button> </> ) }; ``` ### 問題出在 itemStyle? 你可能會想:怎麼可能!`itemStyle` 是一個寫死的 object ,我連動都沒有動,怎麼可能會造成 re-render?但他的問題就是出在,**`itemStyle` 是個 object**。 當 `Table` re-render 時,定義在 `Table` 作用域的 `itemStyle` 也會被**重新定義。**也就是說,`itemStyle` 即便內容一樣,但他還是換了一個新地址。 這就導致舊的 `Row` 傳入的 `itemStyle` 通通跟之前不一樣,`React.memo` 也會認為 props 的確更新了,進而導致沒有變化的 `Row` 通通 re-render 的情況。 前面已提到,diff 演算法是拖累 React 效能的禍首之一。在這個例子下,`React.memo` 不但沒有達到優化的效果 (有用沒用都一樣會 re-render),反而因為加了 `React.memo` ,React 還必須比較前後 props 有無差異,結果**優化不成反而還加重效能負擔**。 ```jsx const Table = ({list}) => { const itemStyle = { color: 'red' }; return ( <table> {list.map(item => <Row key={item.name} item={item} style={itemStyle} />)} </table> ) }; ``` 另外也要注意的是,function 也是 reference type,他也會讓元件重渲染時,導致子元件的 `React.memo` 失去作用: ```jsx const Btn = ({label}) => { /* re-render 就會重新定義 onClick */ const onClick = () => { /* do something... */ }; return ( <Button onClick={onClick}> {label} </Button> ) }; const Btn = ({label}) => { return ( /* 寫在 inline 也會重新定義 onClick function */ <Button onClick={() => {/*do somthing*/}}> {label} </Button> ) } ``` ## useMemo & useCallback 先說結論: > 90% 的情況下你的專案都不需要使用 useMemo & useCallback [^useMemo&Cb] 這兩個 Hook 也是 React 針對效能優化所提供的功能,但他們跟 `React.memo` 不一樣的是,他們是針對 **昂貴的計算結果/function** 進行緩存,只有當 dependency 有所變化時,才會再進行一次重新計算。 **所以 useMemo 跟 useCallback 並不是用來阻止元件 re-render 的!** 你可能會說,不對阿,那我用 useMemo 把值存起來,再傳到子元件裡,下次 re-render 時,props 就不會變動,那元件不就不會 re-render 了嗎? No, No, No,要是這樣我就不用寫這麼多了~ 根據上面這個假設,你的 code 大概會長的像這樣: ```jsx // 1. 利用 useCallback 記憶 onClick 想避免 button re-render const Component = () => { const onClick = useCallback( () => { /* do something */ }, [] ); return ( <> <button onClick={onClick}>Click me</button> ... // 其他元件 </> ); }; // 2. useCallback 依賴 value,所以為了避免 onClick 重新被定義,value 使用 useMemo 緩存 const Item = ({ item, onClick }) => <button onClick={onClick}> {item.name} </button>; const Component = ({ data }) => { const [someStateValue, setSomeStateValue] = useState(); const value = useMemo(() => ({ a: someStateValue }), [someStateValue]); const onClick = useCallback(() => { console.log(value); }, [value]); return ( <> {data.map((d) => (<Item item={d} onClick={onClick} />))} </> ); }; ``` 如果你曾經幹過這種事,恭喜你,上述的優化**通通沒作用**!不只如此,還因為多了一堆比較跟緩存拖慢效能,code 也變得更複雜難解。 上述範例的邏輯建立在下面這個前提: > **只要 props 不改變,元件就不會re-render** > 嘿!還記得前面說 React 怎麼觸發元件 re-render 嗎?我們再來複習一下: > **當 state 被更動或接受到新的 props,就會觸發 re-render 元件** > React 只說了這個情況會觸發 re-render,但是這並不代表,只要 props 不改變,元件就不會 re-render。上面這個假設,實際上是落入了典型的邏輯錯誤, A 是 B 的充分條件,但 !A 不代表是 !B 的充分條件[^useMemo&Cb]。 事實上,只要碰上了**父元件 re-render**,不管你的 props 有沒有改,甚至是沒有 props ,**子元件通通都得 re-render**! 而 React 提供真正能阻止子元件 re-render 的方法只有 `React.memo` 。父元件渲染子元件時,若碰到 `React.memo` 才會中斷子元件的 render,確定 props 有修改,才會繼續 render。 所以 `useMemo` 跟 `useCallback` 若要真正阻止子元件重新渲染,必須搭配 `React.memo`,就像這樣: ```jsx // React.memo 會中斷 re-render 程序,判斷是否需要繼續渲染 const Item = React.memo(({ item, onClick }) => <button onClick={onClick}> {item.name} </button>); const Component = ({ data }) => { const [someStateValue, setSomeStateValue] = useState(); // value 是 onClick 的依賴項,所以也必須緩存 const value = useMemo(() => ({ a: someStateValue }), [someStateValue]); // onClick 必須被緩存,否則每次 Component 重渲染 onClick 就會被重新定義,導致 React.memo 無作用 const onClick = useCallback(() => { console.log(value); }, [value]); return ( <> {data.map((d) => (<Item item={d} onClick={onClick} />))} </> ); }; ``` 也只有在搭配 `React.memo` 的狀態下,使用 `useCallback` 與 `useMemo` 來做 props 的緩存才有意義。**但是你做這樣子的優化付出的代價也相當巨大**。 前面也有提到,React 最花效能的地方是在 **diff 演算法**及 **render DOM** 的步驟,大部分的情況下,記憶這些數值/function 相比之下並沒有太大的優化效果。 大量運算大多會集中在迴圈相關的操作,[這篇文章](https://cloud.tencent.com/developer/article/1967299)針對 useMemo 能優化多少效能做了實驗。 從下面這張圖可以看到,useMemo 雖然在 re-render 階段會優化效能,但只有在 array 長度 > 1000 才有顯著效益,但 useMemo 所帶來的 initial render 的負擔也是非常巨大,array 的長度在 1000 以下,基本上根本不需要 useMemo。 ![useMemo mount 效能會更差,在 re-render 時才有改善效能的作用](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3a7da26becc4493caa84b24313cd2573~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp) *useMemo mount 效能會更差,在 re-render 時才有改善效能的作用* [^useMemo] 況且如果真的需要做這麼大量的計算,也應該要考慮,這些計算在前端做是否合理? ## 小結 經由上面的內容,我們應該要知道,當使用 React.memo、useMemo、useCallback 作為優化的工具時: - `useMemo` 和 `useCallback` 是拿來**緩存**計算數值、function 的工具,並不是使用了 `useMemo` 或 `useCallback` 就可以保證元件不會 re-render - 在一個大前提下:**父元件重新渲染**,除了 `React.memo` ,上述兩個 hook ,對避免 re-render **都不管用**。 - `React.memo` 會在 props 改變前提下重新渲染,因此如果有個元件如下,在 `onClick` 未被 `useCallback` 緩存時,即使這個元件使用了 `React.memo` ,也會因為父元件重新渲染時,又重新定義了 `onClick` 這個 function ,而重新渲染。 ```jsx const Btn = ({onClick}) => React.memo(<Button onClick={onClick} />); const Container = () => { const [isChange, setIsChange] = useState(false); const onClick = () => { setIsChange(!isChange); }; return <Btn onClick={onClick} /> ; ``` - `useMemo` 和 `useCallback` 只有在 re-render 的時候有優化效用,**初始渲染**下,他們不只無用,還可能拖慢 React 的工作,也就是說,若是在不當的使用下,使用這兩個 hook 反而會拖慢效能。 - 使用 `React.memo`、`useMemo`、`useCallback` 都必須謹慎,當你認為你必須使用時,都應該考慮到使用這些方法是在**拿你的記憶體去跟效能做交易**。 # 不需要上述 React 優化函式的優化方案 由前面的敘述可知,state 和 props 變更是觸發 render 的關鍵要素之外,父元件的 re-render 也是造成子元件 re-render 的主要原因,也就是說,透過設計,就算不使用上述 React 提供的優化函式,只要把握不讓 state、props 改變,以及避免父元件 re-render ,也可以達到效能優化的目的。 下面整理出幾種可操作的方式: ## 讓 state 只影響他該影響的範圍 此方法的重點在於,避免 state 影響到其他不相關的元件,比如上面看過的例子: ```jsx const Content = () => <div>I'm Content!</div> const App = () => { const [state, setState] = useState() const onClick = () => setState(!state) return ( <> <button onClick={onClick}>change!</button> <Content /> {/* Content 沒有 props 但也一樣會 rerender*/} </> ) } ``` `Content` 沒有 props 但卻會因為 `state` 變化而重新渲染。此時除了使用 `React.memo` ,你還可以將 `button` 拆成新元件 `Btn`,將 `state` 移到 `Btn` 裡面,如此一來,`state` 只會影響到 `button` 的範圍: ```jsx const Content = () => <div>I'm Content!</div> const Btn = ({children, ...props}) => { const [state, setState] = useState(); const onClick = () => setState(!state); return <button onClick={onClick} {...props}>{children}</button> }; const App = () => { return ( <> <Btn>change!</Btn> <Content /> {/* App 不會 re-render,Content 也不會 */} </> ) }; ``` ## Children props 前面提到 React 的父層元件 re-render 的同時,底下的所有子元件都會跟著 re-render,React.memo 則是為了阻止 prop 沒有改變的子元件也跟著 re-render 所產生出來的解法。但其實我們也可以利用 children props 來阻斷這個 re-render 的連鎖效應。 作法也很簡單,只要將不想受影響的子元件獨立出來,在祖父元件裡使用 children props 傳到原本的父元件即可: ```jsx const Child = () => <p>Hello World!</p> const Parent = ({children}) => { const [count, setCount] = useState(0); return ( <> <button onClick={() => setCount(count + 1 )}>{count}</button> {children} </> ) } const GrantParent = () => { return ( <Parent> <Child /> </Parent> ) } ``` 這個方法不需要 React 記憶 props、比對新舊 props 的差異,只需要調整結構就可以達到避免重渲染的效果。 這個方法也可以優化使用 Context 所影響的子元件 re-render 問題,如果使用 Context 原本的 Code 可能會長這樣[^childrenProps]: ```jsx import { useState, useContext, createContext } from 'react'; const Context = createContext(); const ChildWithCount = () => { const { count, setCount } = useContext(Context); console.log('ChildWithCount re-renders'); return ( <div> <button onClick={() => setCount(count + 1)}>{count}</button> <p>Child</p> </div> ); }; const ExpensiveChild = () => { console.log('ExpensiveChild re-renders'); return <p>Expensive child</p>; }; const CountContext = () => { const [count, setCount] = useState(0); const contextValue = { count, setCount }; return ( <Context.Provider value={contextValue}> <ChildWithCount /> <ExpensiveChild /> // Imagine re-rendering this component is expensive </Context.Provider> ); }; const App = () => { return <CountContext />; }; export default App; ``` 在這種情況下,CountContext 下面的 ChildWithCount 從 context 接收到 setCount ,修改 count 則會導致 CountContext state 改變而造成 re-render,這邊也可以使用 children props 來改善: ```jsx import { useState, useContext, createContext } from 'react'; const Context = createContext(); const ChildWithCount = () => { const { count, setCount } = useContext(Context); console.log('ChildWithCount re-renders'); return ( <div> <button onClick={() => setCount(count + 1)}>{count}</button> <p>Child</p> </div> ); }; const ExpensiveChild = () => { console.log('ExpensiveChild re-renders'); return <p>Expensive child</p>; }; const CountContext = ({ children }) => { const [count, setCount] = useState(0); const contextValue = { count, setCount }; return <Context.Provider value={contextValue}>{children}</Context.Provider>; }; const App = () => { return ( <CountContext> <ChildWithCount /> <ExpensiveChild /> </CountContext> ); }; export default App; ``` ## React Key 有沒有搞錯 React Key 也可以拿來優化效能?的確 React Key 並不是為了優化效能而產生的,但我們可以利用它的特性來達成優化的目的。 首先,我們必須先搞懂 React Key 的工作機制。 ### React Key 的運作原理 在官方文檔中,Key 的描述是寫在 [Rendering List](https://react.dev/learn/rendering-lists#rules-of-keys) 這一章節,在開發過程中,使用 array 形式的 children 也通常會被 console 中的警告要求帶入 `unique “key” prop` ,官方說明使用 keys 有兩個原則: > - **Keys must be unique among siblings.** However, it’s okay to use the same keys for JSX nodes in *different* arrays. > *在兄弟元素之間 Key 必須保持獨特。但是如果是不同的 JSX array 使用相同的 Key 是可以的。* > > - **Keys must not change** or that defeats their purpose! Don’t generate them while rendering. > *Key 一旦設定就不可以改變,否則會破壞使用 Key 的初衷。請避免在渲染時重新產生 key。* > 根據官方文件,使用 Key 的目的是,讓 React 可以從一堆類似的元件中辨識元件,而不是根據他的順序來辨認。React 靠 Key 來辨識元件是否有所更動,進而針對 DOM 進行操作。 前面也提過操作 DOM 是最耗費效能的操作之一。Key 其實是用來幫助 React 優化效能的標誌。 當你沒有設定獨特的 Key 時,React 會自動默認 Array 的 index 當作 key,但 index 會在針對 array 進行排序、插入、刪除時產生變化,React 可能會把分明不相同的元素視為是同一個元素,造成不必要的 re-render,這也是為什麼我們要避免使用 index 當作 Key 的原因之一。 ![元件只是換了順序,元件本身其實不需要 re-render,但因為 React 將 index 相同的元件視為同個元件,認為該元件的值 (Australia ⇒ UK, UK ⇒ Australia) 產生了變化,導致全部元件都 re-render](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/984520e1994b494781ebac9803ae6ee9~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?) *元件只是換了順序,元件本身其實不需要 re-render,但因為 React 將 index 相同的元件視為同個元件,認為該元件的值 (Australia ⇒ UK, UK ⇒ Australia) 產生了變化,導致全部元件都 re-render* [^ReactKey] ![使用唯一值時,component 都不會 re-render,只是更換順序](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1dc099838dbc4122b5919a5f01a79ca6~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?) *使用唯一值時,component 都不會 re-render,只是更換順序* [^ReactKey] 同樣的,隨機產生的 key 也應該避免使用。這會讓 React 無法辨識元件,造成每次 render 都會把原本就的元素刪掉再重新創建元素,即便他們實際上沒有改變。這甚至可能比什麼都不加更糟。 ![component 使用 index 做為 key,只會觸發 re-render](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37c2d7234a8e49c78fd3a917802dc660~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?) *component 使用 index 做為 key,只會觸發 re-render* [^ReactKey] ![使用隨機值作為 key 將永遠不會觸發 re-render](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/24043b27e2cd49d0a8da9b43bc231b82~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?) *使用隨機值作為 key 將永遠不會觸發 re-render* [^ReactKey] 總和來說,React re-render 的過程大致上會是這樣: 1. React 生成元素「之前」和「之後」的快照 2. React 識別頁面中已存在的元素,以便重新使用他們,而非重新創建 1. 如果 `key` 存在,React 會將 「之前」和「之後」元素中,`key` 值相同的元素視為同一個元素 2. 如果 `key` 不存在,React 會默認 index 為 `key` 3. React 辨識完元素後,會開始進行: 1. `unmount` : 刪除 re-render 「之前」存在,但「之後」不存在的元素 2. `mount` : 創建「之前」不存在的元素 3. `re-render`:更新「之前」和「之後」都存在的元素 ### React Key 要怎麼拿來優化效能? 前面我們知道 key 是 React 拿來判斷要將元件 re-render 或 mount 的基準,所以我們可以利用這個特性,讓 React 用 re-render 取代元素被卸載又被重建的行為。 上面提到,使用 index 當作 key 有可能造成不必要的 re-render,但假設你的 array 數量固定、排序不變的狀況下,使用 index 作為 key 會比使用 unique 的 key 來的更好,例如分頁列表就是屬於這種形式: ![Momo購物商品頁](https://i.imgur.com/njDqLrF.png) 由於每一頁的資料都固定是 12 筆,如果使用的是 unique key ,當分頁切換時,第一頁的 12 個商品 component 都會被卸載,並且重新創建第二頁的 12 個商品項目。 但如果使用 index 當作 key,第一頁載入所創建的 12 個商品元件不會被卸載,而是會被當作是資料變更,進行將第二頁資料傳入的 re-render 形式,相比卸載又加載,re-render 更加節省效能。 總結一下,使用 react key 的幾個要點: - **不使用隨機值作為 key** ⇒ 將導致元件永遠都需要重複卸載&加載 - 在列表順序、數量皆保持不變的 **「靜態」列表,使用 index 作為 key** ⇒ 避免複雜元件重複卸載&加載 - 可添加、刪除或修改順序的 **「動態」列表,使用 unique key** ⇒ 可避免不需要的 re-render # Recap - **優化 React 效能的大方向有兩個**: 1. 避免元件 re-render 觸發 diff 演算法,也就是需要避免 state 與 props 的變動 2. 盡量保持 virtualDOM 的一致,避免 ReactDOM 操作 DOM,也就是需要避免重複加載 & 卸載 - **為了提升效能,React 內建提供的優化方法有**: - **React.memo** 會記憶元件的 props,在 re-render 時比較新舊 props 是否有異動,有異動才繼續 re-render 元件。 - **useMemo** 拿來緩存昂貴的計算使用,只在 dependency 變更的狀況下才重新執行,並不能阻止使用該值的元件 re-render。 - **useCallback** 跟 useMemo 類似,拿來緩存在元件內的 callback function,並且只在 dependency 變更的狀況下才重新定義。但一樣不能阻止使用該值的元件 re-render,必須搭配 React.memo 才有效果。 👉 以上 3 種方法都需要 React 撥出額外的記憶體去記憶 & 比較 dependency,使用時必須記得: > **每當使用以上方法,都是在拿你的記憶體跟效能做交易** - **而根據 React 優化的大方向來說,也有不需要使用 React 提供的 function 的方法有**: 1. 調整 component 結構,避免被不相干的 state 影響元件 re-render - 移動 state,讓 state 只存在在需要使用的元件內 - 活用 children props,將不受 state 影響的元件,以 children props 的方式傳入該元件 2. 不使用隨機值作為 key,並可活用 React key 的特性: - 「靜態列表」(數量排序不變,僅內容變化) 使用 **index** key ,可避免重複卸載與加載 (僅 re-render) - 「動態列表」(可增加、刪減、調整排序) 使用 **unique** key,可減少不必要的 re-render # 參考&引用資料 [^childrenProps]:[React components - when do children re-render?](https://whereisthemouse.com/react-components-when-do-children-re-render) [^ReactQuiz]:[React 性能優化大挑戰:一次理解 Immutable data 跟 shouldComponentUpdate](https://blog.huli.tw/2018/01/15/react-performance-immutable-and-scu/) [^useMemo]:[【译】你真的应该使用useMemo吗? 让我们一起来看看 - 掘金](https://juejin.cn/post/6969874579028197413?searchId=202311020934016CB13EE5CB8BE9F008DC) [^useMemo&Cb]:[「好文翻译」为什么你可以删除 90% 的 useMemo 和 useCallback ? - 掘金](https://juejin.cn/post/7251802404877893689) [^ReactKey]:[「好文翻译」React key属性:高性能列表的最佳实践 - 掘金](https://juejin.cn/post/7257022428194521145)