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