如何做 React 性能優(yōu)化?
大家好,我是前端西瓜哥。今天帶大家來(lái)學(xué)習(xí)如何做 React 性能優(yōu)化。
使用 React.memo()
一個(gè)組件可以通過(guò) React.memo 方法得到一個(gè)添加了緩存功能的新組件。
const Comp = props => { //}const MemorizedComp = React.memo(Comp);
再次渲染時(shí),如果 props 沒(méi)有發(fā)生改變,就跳過(guò)該組件的重渲染,以實(shí)現(xiàn)性能優(yōu)化。
這里的 關(guān)鍵在于 props 不能改變,這也是最?lèi)盒牡牡胤健?/p>
對(duì)于像是字符串、數(shù)值這些基本類(lèi)型,對(duì)比沒(méi)有問(wèn)題。但對(duì)于對(duì)象類(lèi)型,就要做一些緩存工作,讓原本沒(méi)有改變的對(duì)象或函數(shù)仍舊指向同一個(gè)內(nèi)存對(duì)象。
因?yàn)槊看魏瘮?shù)組件被執(zhí)行時(shí),里面聲明的函數(shù)都是一個(gè)全新的函數(shù),和原來(lái)的函數(shù)指向不同的內(nèi)存空間,全等比較結(jié)果是 false。
處理 props 比較問(wèn)題
React.memo() 最疼痛的就是處理 props 比較問(wèn)題。
我們看個(gè)例子:
const MemorizedSon = React.memo(({ onClick }) => { // ...})const Parent() { // ... const onClick = useCallback(() => { // 一些復(fù)雜的判斷和邏輯 }, [a, setA, b, onSava]); return ( <div> <MemorizedSon onClick={onClick} /> </div> )}
上面為了讓函數(shù)的引用不變,使用了 useCallback。函數(shù)里用到了一些變量,因?yàn)楹瘮?shù)組件有閉包陷阱,可能會(huì)導(dǎo)致指向舊狀態(tài)問(wèn)題,所以需要判斷這些變量是否變化,來(lái)決定是否使用緩存函數(shù)。
這里就出現(xiàn)了一個(gè) 連鎖反應(yīng),就是我還要給變量中的對(duì)象類(lèi)型做緩存,比如這里的 setA 和 onSave 函數(shù)。然后這些函數(shù)可以又依賴(lài)其他函數(shù),一直連鎖下去,然后你發(fā)現(xiàn)有些函數(shù)甚至來(lái)自其他組件,通過(guò) props 注入。
啊我真的是麻了呀,我優(yōu)雅的 React 一下變得丑陋不堪。
怎么辦,一個(gè)方式是用 ref。ref 沒(méi)有閉包問(wèn)題,且能夠在組件每次更新后保持原來(lái)的指向。
const MemorizedSon = React.memo(({ onClickRef }) => { const onClick = onClickRef.current;})const Parent() { // ... const onClick = () => { // 一些復(fù)雜的判斷和邏輯 }; const onClickRef = useRef(onClick); onClickRef.current = onClick; return ( <div> <MemorizedSon onClickRef={onClickRef} /> </div> )}
或者
const MemorizedSon = React.memo(({ onClick }) => { // ...})const Parent() { // ... const onClick = useCallback(() => { const {a, b} = propsRef.current; const {setA, setSave} = stateRef.current; // 一些復(fù)雜的判斷和邏輯 }, []); return ( <div> <MemorizedSon onClick={onClick} /> </div> )}
當(dāng)然官方也注意到這種場(chǎng)景,提出了 useEvent 的提案,希望能盡快實(shí)裝吧。
function Chat() { const [text, setText] = useState(''); const onClick = useEvent(() => { sendMessage(text); }); return <SendButton onClick={onClick} />;}
The code inside useEvent “sees” the props/state values at the time of the call. The returned function has a stable identity even if the props/state it references change. There is no dependency array。
用了 useEvent 后,指向是穩(wěn)定的,不需要加依賴(lài)項(xiàng)數(shù)組。
提案詳情具體看下面這個(gè)鏈接:
https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md。
跳過(guò)中間組件
假設(shè)我們的組件嵌套是這樣的:A -> B -> C。
其中 C 需要拿到 A 的一個(gè)狀態(tài)。B 雖然不需要用到 A 的任何狀態(tài),但為了讓 C 拿到狀態(tài),所以也用 props 接收了這個(gè),然后再傳給 C。
這樣的話,A 更新?tīng)顟B(tài)時(shí),B 也要進(jìn)行不必要的重渲染。
對(duì)于這種情況,我們可以讓中間組件 B 跳過(guò)渲染:
- 給 B 應(yīng)用 React.memo,A 的狀態(tài)不再傳給 B。
- A 的狀態(tài)通過(guò)發(fā)布訂閱的方式傳給 C(比如 useContext,或通過(guò)狀態(tài)管理)。
狀態(tài)下放
假設(shè)同樣還是 A -> B -> C 形式的組件嵌套。
C 需要來(lái)自 A 的狀態(tài),B 會(huì)幫忙通過(guò) props 傳遞狀態(tài)過(guò)來(lái)。A 狀態(tài)更新時(shí),A、B、C 都會(huì)重渲染。
如果狀態(tài)只有 C 一個(gè)組件會(huì)用到,我們可以考慮直接把狀態(tài)下放到 C。這樣當(dāng)狀態(tài)更新時(shí),就只會(huì)渲染 C。
組件提升
將組件提升到父組件的 props 上。
export default function App() { return ( <ColorPicker> <p>Hello, world!</p> <ExpensiveTree /> </ColorPicker> );}function ColorPicker({ children }) { let [color, setColor] = useState("red"); return ( <div style={{ color }}> <input value={color} onChange={(e) => setColor(e.target.value)} /> {children} </div> );}
在這里 ColorPicker 更新 color 狀態(tài)后,因?yàn)?ExpensiveTree 來(lái)自外部 props,不會(huì)改變,不會(huì)重渲染。除非是 App 中發(fā)生了狀態(tài)改變。
正確使用列表 key
進(jìn)行列表渲染時(shí),React 會(huì)要求你給它們提供 key,讓 React 識(shí)別更新后的位置變化,避免一些不必要的組件樹(shù)銷(xiāo)毀和重建工作。
比如你的第一個(gè)元素是 div,更新后發(fā)生了位置變化,第一個(gè)變成了 p。如果你不通過(guò) key 告知新位置,React 就會(huì)將 div 下的整棵樹(shù)銷(xiāo)毀,然后構(gòu)建 p 下的整棵樹(shù),非常耗費(fèi)性能。
如果你提供了位置,React 就會(huì)做真實(shí) DOM 的位置移動(dòng),然后做樹(shù)的更新,而不是銷(xiāo)毀和重建。
注意狀態(tài)管理庫(kù)的觸發(fā)更新機(jī)制
對(duì)于使用 Redux 的進(jìn)行狀態(tài)管理的朋友來(lái)說(shuō),我們會(huì)在函數(shù)組件中通過(guò) useSelector 來(lái)訂閱狀態(tài)的變化,自動(dòng)更新組件。
const Comp() { const count = useSelector(state => state.count);}
useSelector 做了什么事?它會(huì)訂閱 state 的變化,當(dāng) state 變化時(shí),會(huì)運(yùn)行回調(diào)函數(shù)得到返回值,和上一次的返回值進(jìn)行全等比較。如果相等,不更新組件;如果不等,更新組件。然后緩存這次的返回值。
上面這種情況還好,我們?cè)倏纯磳?xiě)成對(duì)象的形式。
import { shallowEqual, useSelector } from 'react-redux'const Comp() { const { count, username } = useSelector(state => ({ count: state.count, username: state.username }), shallowEqual);}
上面這種寫(xiě)法,因?yàn)槟J(rèn)用的是全等比較,所以每次 state 更新后比較結(jié)果都是 false,組件每次都更新。對(duì)于組合成的對(duì)象,你要用 shallowEqual 淺比較來(lái)替代全等比較,避免不必要的更新。
有一種情況比較特別,假設(shè) state.userInfo 有多個(gè)屬性,username、age、acount、score、level 等。有些人會(huì)這樣寫(xiě):
const Comp() { const { username, age } = useSelector(state => state.userInfo), shallowEqual);}
看起來(lái)沒(méi)什么問(wèn)題,但里面是有陷阱的:雖然我們的組件只用到 username 和 age,但 useSelector 卻會(huì)對(duì)整個(gè) userInfo 對(duì)象做比較。
假設(shè)我們只更新了 userInfo.level,useSelector 的比較結(jié)果就為 false 了,導(dǎo)致組件更新,即使你沒(méi)有用上 level,這不是我們期望的。
所以正確的寫(xiě)法應(yīng)該是:
const Comp() { const { username, age } = useSelector(state => { const { username, age } = state.userInfo; return { username, age }; }), shallowEqual);}
使用 useSelector 監(jiān)聽(tīng)狀態(tài)變化,一定要關(guān)注 state 的粒度問(wèn)題。
Context 是粗粒度的
React 提供的 Context 的粒度是粗粒度的。
當(dāng) Context 的值變化時(shí),用到該 Context 的組件就會(huì)更新。
有個(gè)問(wèn)題,就是 我們提供的 Context 值通常都是一個(gè)對(duì)象,比如:
const App = () => { return ( <EditorContext.Provider value={ visible, setVisible }> <Editor /> </EditorContext.Provider> );}
每當(dāng) Context 的 value 變化時(shí),用到這個(gè) Context 的組件都會(huì)被更新,即使你只是用這個(gè) value 的其中一個(gè)屬性,且它沒(méi)有改變。
因?yàn)?nbsp;Context 是粗粒度的。
所以你或許可以考慮在高一些層級(jí)的組件去獲取 Context,然后通過(guò) props 分別注入到用到 Context 的不同部分的組件中。
順便一提,Context 的 value 在必要時(shí)也要做緩存,以防止組件的無(wú)意義更新。
const App = () => { const EditorContextVal = useMemo(() => ({ visible, setVisible }), [visible, setVisible]); return ( <EditorContext.Provider value={ visible, setVisible }> <Editor /> </EditorContext.Provider> );}
批量更新
有一個(gè)經(jīng)典的問(wèn)題是:React 的 setState 是同步還是異步的?
答案是副作用或合成事件響應(yīng)函數(shù)內(nèi),是異步的,會(huì)批量執(zhí)行。其他情況(比如 setTimeout)則會(huì)同步執(zhí)行,同步的問(wèn)題是會(huì)立即進(jìn)行組件更新渲染,一次有多個(gè)同步 setState 就可能會(huì)有性能問(wèn)題。
我們可以用ReactDOM.unstable_batchedUpdates 來(lái)將本來(lái)需要同步執(zhí)行的狀態(tài)更新變成批量的。
ReactDOM.unstable_batchedUpdates(() => { setScore(score + 1); setUserName('前端西瓜哥');})
不過(guò)到了 React18 后,開(kāi)啟并發(fā)模式的話,就沒(méi)有同步問(wèn)題了,所有的 setState 都是異步的。
Redux 的話,你可以考慮使用批量更新插件:redux-batched-actions。
import { batchActions } from 'redux-batched-actions';dispatch(batchActions([ setScoreAction(score + 1), setUserName('前端西瓜哥')]));
redux-batched-actions 中間件確實(shí)會(huì)將多個(gè) actions 做一個(gè)打包組合再 dispatch,你會(huì)發(fā)現(xiàn) store.subscribe 的回調(diào)函數(shù)觸發(fā)次數(shù)確實(shí)變少了。
但如果你用了 react-redux 庫(kù)的話,這個(gè)庫(kù)其實(shí)在多數(shù)情況下并沒(méi)有什么用。
因?yàn)?react-redux 其實(shí)已經(jīng)幫我們做了批量處理操作,同步的多個(gè) dispatch 執(zhí)行完后,才會(huì)通知組件進(jìn)行重渲染。
懶加載
有些組件,如果可以的話,可以讓組件直接不渲染,做一個(gè)懶加載。比如:
{visible && <Model />}
結(jié)尾
React 的優(yōu)化門(mén)道還是挺多的,其中的 React.memo 優(yōu)化起來(lái)確實(shí)復(fù)雜,一不小心還會(huì)整成負(fù)優(yōu)化。
所以,不要 過(guò)早進(jìn)行優(yōu)化。