這才是 React Hooks 性能優(yōu)化的正確姿勢
React Hooks
出來很長一段時間了,相信有不少的朋友已經(jīng)深度使用了。無論是 React
本身,還是其生態(tài)中,都在摸索著進(jìn)步。鑒于我使用的 React
的經(jīng)驗,給大家分享一下。對于 React hooks
,性能優(yōu)化可以從以下幾個方面著手考慮。
場景1
在使用了 React Hooks
后,很多人都會抱怨渲染的次數(shù)變多了。沒錯,官方就是這么推薦的:
我們推薦把 state
切分成多個 state
變量,每個變量包含的不同值會在同時發(fā)生變化。
- function Box() {
- const [position, setPosition] = useState({ left: 0, top: 0 });
- const [size, setSize] = useState({ width: 100, height: 100 });
- // ...
- }
這種寫法在異步的條件下,比如調(diào)用接口返回后,同時 setPosition
和 setSize
,相較于 class 寫法會額外多出一次 render
。這也就是渲染次數(shù)變多的根本原因。當(dāng)然這種寫法仍然值得推薦,可讀性和可維護(hù)性更高,能更好的邏輯分離。
針對這種場景若出現(xiàn)十幾或幾十個 useState
的時候,可讀性就會變差,這個時候就需要相關(guān)性的組件化了。以邏輯為導(dǎo)向,抽離在不同的文件中,借助 React.memo
來屏蔽其他 state
導(dǎo)致的 rerender
。
- const Position = React.memo(({ position }: PositionProps) => {
- // position 相關(guān)邏輯
- return (
- <div>{position.left}</div>
- );
- });
因此在 React hooks
組件中盡量不要寫流水線代碼,保持在 200 行左右最佳,通過組件化降低耦合和復(fù)雜度,還能優(yōu)化一定的性能。
場景2
class
對比 hooks
,上代碼:
- class Counter extends React.Component {
- state = {
- count: 0,
- };
- increment = () => {
- this.setState((prev) => ({
- count: prev.count + 1,
- }));
- };
- render() {
- const { count } = this.state;
- return <ChildComponent count={count} onClick={this.increment} />;
- }
- }
- function Counter() {
- const [count, setCount] = React.useState(0);
- function increment() {
- setCount((n) => n + 1);
- }
- return <ChildComponent count={count} onClick={increment} />;
- }
憑直觀感受,你是否會覺得 hooks
等同于 class
的寫法?錯, hooks
的寫法已經(jīng)埋了一個坑。在 count
狀態(tài)更新的時候, Counter
組件會重新執(zhí)行,這個時候會重新創(chuàng)建一個新的函數(shù) increment
。這樣傳遞給 ChildComponent
的 onClick
每次都是一個新的函數(shù),從而導(dǎo)致 ChildComponent
組件的 React.memo
失效。
解決辦法:
- function usePersistFn<T extends (...args: any[]) => any>(fn: T) {
- const ref = React.useRef<Function>(() => {
- throw new Error('Cannot call function while rendering.');
- });
- ref.current = fn;
- return React.useCallback(ref.current as T, [ref]);
- }
- // 建議使用 `usePersistFn`
- const increment = usePersistFn(() => {
- setCount((n) => n + 1);
- });
- // 或者使用 useCallback
- const increment = React.useCallback(() => {
- setCount((n) => n + 1);
- }, []);
上面聲明了 usePersistFn
自定義 hook
,可以保證函數(shù)地址在本組件中永遠(yuǎn)不會變化。完美解決 useCallback
依賴值變化而重新生成新函數(shù)的問題,邏輯量大的組件強烈建議使用。
不僅僅是函數(shù),比如每次 render
所創(chuàng)建的新對象,傳遞給子組件都會有此類問題。盡量不在組件的參數(shù)上傳遞因 render
而創(chuàng)建的對象,比如 style={{ width: 0 }}
此類的代碼用 React.useMemo
或 React.memo
編寫 equal
函數(shù)來優(yōu)化。
style
若不需改變,可以提取到組件外面聲明。盡管這樣做寫法感覺太繁瑣,但是不依賴 React.memo
重新實現(xiàn)的情況下,是優(yōu)化性能的有效手段。
- const style: React.CSSProperties = { width: 100 };
- function CustomComponent() {
- return <ChildComponent style={style} />;
- }
場景3
對于復(fù)雜的場景,使用 useWhyDidYouUpdate
hook 來調(diào)試當(dāng)前的可變變量引起的 rerender
。這個函數(shù)也可直接使用 ahooks
中的實現(xiàn)。
- function useWhyDidYouUpdate(name, props) {
- const previousProps = useRef();
- useEffect(() => {
- if (previousProps.current) {
- const allKeys = Object.keys({ ...previousProps.current, ...props });
- const changesObj = {};
- allKeys.forEach(key => {
- if (previousProps.current[key] !== props[key]) {
- changesObj[key] = {
- from: previousProps.current[key],
- to: props[key]
- };
- }
- });
- if (Object.keys(changesObj).length) {
- console.log('[why-did-you-update]', name, changesObj);
- }
- }
- previousProps.current = props;
- });
- }
- const Counter = React.memo(props => {
- useWhyDidYouUpdate('Counter', props);
- return <div style={props.style}>{props.count}</div>;
- });
當(dāng) useWhyDidYouUpdate
中所監(jiān)聽的 props 發(fā)生了變化,則會打印對應(yīng)的值對比,是調(diào)試中的神器,極力推薦。
場景4
借助 Chrome Performance 代碼進(jìn)行調(diào)試,錄制一段操作,在 Timings
選項卡中分析耗時最長邏輯在什么地方,會展現(xiàn)出組件的層級棧,然后精準(zhǔn)優(yōu)化。
場景5
在 React
中是極力推薦函數(shù)式編程,可以讓數(shù)據(jù)不可變性作為我們優(yōu)化的手段。我在 React
class 時代大量使用了 immutable.js
結(jié)合 redux
來搭建業(yè)務(wù),與 React
中 PureComponnet
完美配合,性能保持非常好。但是在 React hooks
中再結(jié)合 typescript
它就顯得有點格格不入了,類型支持得不是很完美。這里可以嘗試一下 immer.js
,引入成本小,寫法也簡潔了不少。
- const nextState = produce(currentState, (draft) => {
- draft.p.x.push(2);
- })
- // true
- currentState === nextState;
場景6
復(fù)雜場景使用 Map
對象代替數(shù)組操作, map.get()
, map.has()
,與數(shù)組查找相比尤其高效。
- // Map
- const map = new Map([['a', { id: 'a' }], ['b', { id: 'b' }], ['c', { id: 'c' }]]);
- // 查找值
- map.has('a');
- // 獲取值
- map.get('a');
- // 遍歷
- map.forEach(n => n);
- // 它可以很容易轉(zhuǎn)換為數(shù)組
- Array.from(map.values());
- // 數(shù)組
- const list = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
- // 查找值
- list.some(n => n.id === 'a');
- // 獲取值
- list.find(n => n.id === 'a');
- // 遍歷
- list.forEach(n => n);
結(jié)語
React 性能調(diào)優(yōu),除了阻止 rerender
,還有與寫代碼的方式有關(guān)系。最后,我要推一下近期寫的 React
狀態(tài)管理庫 https://github.com/MinJieLiu/heo,也可以作為性能優(yōu)化的一個手段,希望大家從 redux
的繁瑣中解放出來,省下的時間用來享受生活。