[note] React Performance
React Performance @ Frontend Master
原則
- 不額外做事總是比做事來的快:如果能透過「調整 component 階層或 state」就解決問題的話,這是第一優先選擇
- 當客觀數據顯示利大於弊時,才考慮用「memoization」
- 有些事情可以延後執行,則可以使用 Suspense API
- 先執行緊急的事情,接著再執行較不緊急的事情,這可以使用「Transition API」
React Developer Tools: Profiler 的使用
Top Bar
- 觀察的重點是 Render duration
Flame graph
點擊每一個 bar 會看到每次 render 時不同的 flame graph,寬度約寬表示所需的時間越久,並且可以看到導致拖這麽久的元素是什麼:
Ranked
點擊每一個 bar 會看到每次 render 時不同的 Ranked ,排序後可以很清楚知道,有問題的 component 是 ExpensiveComponent
:
Timeline
從 Timeline 也可以清楚看到導致 render 時間拉長的原因:
觀察問題
Chrome Performance: 出現紅標
Chrome 的 Performance Tools 會針對有問題的部分標上「紅標」:
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 以上,表示有明顯的效能問題:
從 Ranked 頁籤可以很明確知道問題是發生在那個 component:
從 Timeline 的頁籤也可以看到每次 render 都花了超過 300ms 的時間:
比較正常的情況應該要是,第一次載入因為有做複雜的運算,所以花了比較久的時間,但後續 UI re-render 時,都不應該超過 16ms:
當使用者操作時有多少元件 re-render
把 "Highlight updates when components render" 打開:
接著在和畫面互動時,可以很明確的看到哪些 component 會 re-render:
最理想的情況是,真的值有改變的 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 傳下去,因為 setState
和 dispatch
一樣都是 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
- React.PureComponent @ react.js
- shallow-compare @ react.js
- shouldComponentUpdate in action @ react.js > performance
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
, useReducer
或 useContext
的話,當 state 或 context 有改變時,這個元件依然會重新渲染。
和 React.PureComponent 一樣,React.memo 也是使用 shallow compare 的方式在比對物件是否相同。
Compare Algorithm in React
shallow-compare
- shallow-compare @ react.js
- shallowEqual.js @ react > packages > shared
實際上,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
- objectIs.js @ react > packages > shared
- Object.is @ MDN
Object.is()
可以用來比較兩個值是否相同。Object.is()
和 ===
的差別只有在判斷 +0
、-0
和 NaNs
是會有不同:
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