跳至主要内容

[note] React Performance

參考資料

React Performance @ Frontend Master

原則

  1. 不額外做事總是比做事來的快:如果能透過「調整 component 階層或 state」就解決問題的話,這是第一優先選擇
  2. 當客觀數據顯示利大於弊時,才考慮用「memoization」
  3. 有些事情可以延後執行,則可以使用 Suspense API
  4. 先執行緊急的事情,接著再執行較不緊急的事情,這可以使用「Transition API」

React Developer Tools: Profiler 的使用

Top Bar

  • 觀察的重點是 Render duration

image-20240112000833578

Flame graph

點擊每一個 bar 會看到每次 render 時不同的 flame graph,寬度約寬表示所需的時間越久,並且可以看到導致拖這麽久的元素是什麼:

image-20240112001252176

Ranked

點擊每一個 bar 會看到每次 render 時不同的 Ranked,排序後可以很清楚知道,有問題的 component 是 ExpensiveComponent

image-20240112001401433

Timeline

從 Timeline 也可以清楚看到導致 render 時間拉長的原因:

image-20240112001431981

觀察問題

Chrome Performance: 出現紅標

Chrome 的 Performance Tools 會針對有問題的部分標上「紅標」:

image-20240110122332045

React Profiler: Render 的時間超過16ms

一般來說,要讓使用者用起來覺得順暢,至少要 60 FPS(frames per second),1000ms/60 = 16.67ms,也就是 render 的時間不應該超過 16ms。

如果從 React Profiling Tools 發現它 render 的時間超過 16ms,像是下圖,除了第一次 render 需要 300ms 外,每次畫面 re-render 也需要 300ms 以上,表示有明顯的效能問題:

image-20240110120237768

從 Ranked 頁籤可以很明確知道問題是發生在那個 component:

image-20240112000450768

從 Timeline 的頁籤也可以看到每次 render 都花了超過 300ms 的時間:

image-20240110133921837

比較正常的情況應該要是,第一次載入因為有做複雜的運算,所以花了比較久的時間,但後續 UI re-render 時,都不應該超過 16ms:

image-20240110120414336

image-20240110134023594

當使用者操作時有多少元件 re-render

把 "Highlight updates when components render" 打開:

image-20240114113739347

接著在和畫面互動時,可以很明確的看到哪些 component 會 re-render:

image-20240114120237768

最理想的情況是,真的值有改變的 component 才去 re-render(有藍框框),其他無關的元件則最好不要有藍框框出現。

常見的解決方式

避免需要耗費資源的 component 被無意義的重新 render

方法:把會導致 re-render 的元件放到越底層越好,減少被它影響的 scope

方法:把子層元件當成 children 用 props 傳進來後,就可以避免 re-render

https://www.facebook.com/groups/reacthooks/posts/881649496400156/

方法:使用 React.memo

Code Splitting

使用 React 提供的 Suspense 和 Lazy

使用 React 內建的 API

React.memo

如果 props 是會一直改變的(例如,有用到 children),用 React.memo 沒有太大的意義。

useCallback 和 useMemo

useReducer

如果需要把 parent 的多個 methods 傳到子層,一種方式是使用 useCallback 把這些 methods 記下來;另一種方式則是使用 useReducer,如此可以只傳 dispatch 到子層,不需要另外使用 useCallback

提示

另一種不用 useReducer 但又可以避免因為 method 改變而 re-render 的方式,是直接把 setState 的 setter 透過 props 傳下去,因為 setStatedispatch 一樣都是 immutable 的,所以不會導致不必要的 re-render。

這種方式特別適合用在但 useCallback 的 dependency arrays 幾乎會一直改變時,例如:

// 雖然用了 useCallback,但這個 function 執行的時候,會導致 items 改變,使得這個 useCallback 本身沒有太大的效果
const App = () => {
const remove = useCallback(
(id) => {
setItems(removeItem(items, id));
},
[items],
);

return (
// 這個 remove 因為相依於 items,所以會一直被 mutate
<SomeComponent remove={remove} />
);
};

更好的作法可以利用 dispatch 不會 mutate 的特性,把 dispatch 傳下去:

const App = () => {
const [items, dispatch] = useReducer(reducer, getInitialItems());

return (
// 只把 dispatch 傳下去,dispatch 不會 mutate
<SomeComponent dispatch={dispatch} />
);
};

API

Pure Component

Pure Component 的意思和 Pure Function 類似,當某個 component 只要有相同的 input(state 和 props)時,就會回傳相同內容時,即稱作 Pure Component。

當我們確定某個 React 元件符合 Pure Component 的情況時,就可以讓該元件繼承 React.PureComponent 而不是 React.Component,PureComponent 比起一般的 Component 幫忙實作了 shouldComponentUpdate 的方法,它會透過 shallow-compare 的方式 state 和 props 有無差異後來決定要不要更新該 component。

React.memo

memo @ react.dev

當 React 元件在有相同的 props 就會 render 相同的內容時,可以透過 React.memo() 來提升效能。

React.memo() 只會檢查 props 的差異,如果你在該元件中有使用到 useState, useReduceruseContext 的話,當 state 或 context 有改變時,這個元件依然會重新渲染。

和 React.PureComponent 一樣,React.memo 也是使用 shallow compare 的方式在比對物件是否相同。

Compare Algorithm in React

shallow-compare

實際上,shallowCompare 底層是用 shallowEqual 的方式在進行比對,而 shallowEqual 這個方法會(其程式碼可以參考這個 gist):

  • 針對 native types 先用 Object.is 對兩個值做判斷,如果是 true 就回傳
  • 再比較是否是相同的物件(只會比到第一層)或陣列
// native type
shallowEqual('foo', 'foo'); // true

// object
shallowEqual({ foo: 'foo' }, { foo: 'foo' }); // true
shallowEqual({ foo: { bar: 'bar' } }, { foo: { bar: 'bar' } }); // false

// array
shallowEqual(['foo'], ['foo']); // true
shallowEqual(['foo'], ['foo', 'bar']); // false

Object.is

Object.is() 可以用來比較兩個值是否相同。Object.is()=== 的差別只有在判斷 +0-0NaNs 是會有不同

Object.is(0, -0); // false
Object.is(+0, -0); // false
0 === -0; // true

Object.is(NaN, 0 / 0); // true
NaN === 0 / 0; // false

Object.is(NaN, Number.NaN); // true
NaN === Number.NaN; // false