距離我接觸 react 已經過去幾個月了,在此期間,關於如何避免重複渲染的問題一直困惑著我,因此今天就來聊聊這個話題。
在講述如何進行性能優化之前,我們先來談談 React 為什麼會重新渲染。
React 為什麼會重新渲染#
狀態改變是 React 樹內部發生更新的唯二原因之一。
import { useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree />
</div>
);
};
const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延遲
}
console.log('render');
return <p>I am a very slow component tree.</p>;
};
很明顯,每當我們在 input 裡面輸入內容,console.log('render')
都會輸出,因為 color
的狀態發生了改變,也間接說明了其與 props 完全沒有關係
這樣的開銷是很不合理的,我們接下來會優化它。
性能優化#
方式一: State 抽離#
我們知道 react 是單向數據流,因此我們只需要將 State 的抽離出來即可。
import { useState } from "react";
const App = () => {
return (
<div>
<Input />
<ExpensiveTree />
</div>
);
};
const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延遲
}
console.log("render");
return <p>I am a very slow component tree.</p>;
};
const Input = () => {
let [color, setColor] = useState("red");
return <input value={color} onChange={(e) => setColor(e.target.value)} />;
}
方式二: memo#
React.memo 其為高階組件,可以使被它包裹的組件變為純組件,也就是只要它的 prop 不改變,react 就不會更新它。
import { memo, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree />
</div>
);
};
const ExpensiveTree = memo(() => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延遲
}
return <p>I am a very slow component tree.</p>;
})
方式三: react children#
因為 App 並沒有發生狀態改變所以 ExpensiveTree 避免了重複渲染
import { FC, PropsWithChildren, useState } from "react";
const App = () => {
return (
<ColorWrapper>
<ExpensiveTree />
</ColorWrapper>
);
};
const ColorWrapper: FC<PropsWithChildren> = ({ children }) => {
let [color, setColor] = useState("red");
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children}
</div>
);
};
const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延遲
}
return <p>I am a very slow component tree.</p>;
};
使用 useMemo 和 useCallback#
useMemo#
useMemo 有點類似於 vue 中的 Computed
,只有當依賴變化時,才會重新計算出新的值。
這樣當 input 發生改變的時候 dirtyWork 就不會重複的去執行。
import { useMemo, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
const [number,setNumber] = useState(0)
const dirtyWork = useMemo(() => {
console.log('正在進行大量運輸');
return number
},[number])
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<h1>{dirtyWork}</h1>
</div>
);
};
另外上一節的例子我們也可以透過 useMemo 進行修改
import { memo, useMemo, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{useMemo(
() => (
<ExpensiveTree />
),
[]
)}
</div>
);
};
const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延遲
}
return <p>I am a very slow component tree.</p>;
};
useCallback#
我們先看如下例子
import { FC, memo, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
const fn = ()=> {
console.log('hahaha');
}
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree fn={fn}/>
</div>
);
};
const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延遲
}
console.log('render'); // 依舊會被不斷更新
return <p>I am a very slow component tree.</p>;
})
我們發現即使 ExpensiveTree 包裹 memo ,但在 input 裡面輸入內容, ExpensiveTree 依舊會被更新,這時我們只要給父組件 fn 函數包裹一層 useCallback 即可
因此 useCallback 一般用於需要將函數傳遞給子組件的情況,我們用 useCallback 改寫上面的例子:
import { FC, memo, useCallback, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
const fn = useCallback(()=> {
console.log('hahaha');
},[])
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree fn={fn}/>
</div>
);
};
const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延遲
}
console.log('render');
return <p>I am a very slow component tree.</p>;
})
你可能會發現 useCallback 其實就是 useMemo 的語法糖,如上例子也可以使用 useMemo 改寫
import { FC, memo, useMemo, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
const fn = useMemo(() => {
return () => console.log("hahaha");
}, []);
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree fn={fn} />
</div>
);
};
const ExpensiveTree: FC<{ fn: () => void }> = memo(({ fn }) => {
let now = performance.now();
while (performance.now() - now < 100) {
// 延遲
}
console.log("render");
return <p>I am a very slow component tree.</p>;
});