如何方便的檢測React項目的性能?
大家好,我卡頌。
對于長期迭代的React項目,性能是不能忽視的問題。通常我們通過:
- React-Dev-Tools的Profiler面板
- 一些第三方工具,比如why-did-you-render[1]
檢測運行時性能瓶頸。
實際上,React本身就內置了性能檢測組件 —— Profiler,可以很方便的檢測React項目的性能。
使用方式
Profiler是個內置組件,用他包裹需要檢測性能的組件即可:
<Profiler id="App" onRender={onRender}>
<App />
</Profiler>
嵌套使用也是可以的:
<App>
<Profiler id="Sidebar" onRender={onRender}>
<Sidebar />
</Profiler>
<Profiler id="Content" onRender={onRender}>
<Content />
</Profiler>
</App>
Profiler會檢測被他包裹的組件樹的性能,檢測結果會作為onRender回調的參數:
function onRender(
id,
phase,
actualDuration, baseDuration,
startTime, commitTime
) {
// ...回調
}
那么,這些參數都是什么意思呢?其實我們完全沒必要記這些。
我們只需要知道,一些典型的性能優化場景該使用哪些參數就行。
場景1:組件是不是嵌套更新?
對于一般的組件更新,會經歷4個步驟:
- 組件觸發更新
- 計算更新的影響
- 執行DOM操作
- 視圖更新
但如果在上一次更新流程的4個步驟還未走完的情況下,又觸發新的更新:
可以發現,在這種情況下,「視圖更新」的時機遠滯后于一般更新流程,這會造成頁面交互卡頓。
這就是「組件嵌套更新」,通常我們在useLayoutEffect中觸發新的更新會遇到這種情況。
Profiler onRender回調的phase參數,用來表示組件所處更新階段:
- mount,代表組件是首屏渲染。
- update,代表組件更新。
- nested-update,代表組件嵌套更新。
通過該參數可以判斷組件是否處于嵌套更新。
當遇到嵌套更新造成的性能問題,可以考慮用useEffect替代useLayoutEffect。
場景2:性能優化到底起沒起作用?
當提到「性能優化」,很多同學第一反應就是:
- useCallback
- useMemo
- React.memo
但當我們使用這些性能優化API后,我們怎么知道性能是否變得更好?
為了檢測優化效果,通常會在關鍵組件打印個log,如果狀態更新后log沒打印,代表組件沒有render,命中緩存成功,比如這樣:
function App() {
console.log('App render')
// ...省略邏輯
}
但這樣并不能反映性能優化的整體效果。這時候可以考慮Profiler中的actualDuration與baseDuration參數:
- baseDuration衡量組件子樹在不命中任何緩存時,完整render一次所花時間。
- actualDuration衡量組件子樹實際完整render一次所花時間。
所以,用baseDuration減去actualDuration剩余的時間,就是性能優化節約的時間。
比如,對下面的<App/>組件性能優化后,只需要在onRender中比較baseDuration與actualDuration之間的差就能度量<App />的性能優化效果:
<Profiler id="App" onRender={onRender}>
<App />
</Profiler>
這個值越高,代表性能優化效果越好。當接近0時,代表性能優化沒有起到作用。
需要注意的是,baseDuration是通過子樹中每個組件最近render所需時間匯總求和得到的近似值,有時并不準確
如果你的同事固執的認為所有函數props都必須用useCallback包裹,所有變量props都必須用useMemo包裹,請用以上數據狠狠的和他講道理。
場景3:項目的性能瓶頸在哪?
當我們要做性能優化時,首先應該明確 —— 項目的性能瓶頸在哪?此時,可以用Profiler劃分幾個「待比較區域」,再分別對比actualDuration。
比如,對于下面的應用:
<App>
<Sidebar />
<Content />
</App>
<Sidebar />和<Content />誰render更耗時?
此時可以用Profiler分別包裹這兩個組件:
<App>
<Profiler id="Sidebar" onRender={onRender}>
<Sidebar />
</Profiler>
<Profiler id="Content" onRender={onRender}>
<Content />
</Profiler>
</App>
再分別在onRender中衡量actualDuration,值比較高的區域render更耗時。
這種方式定制性比較高。如果想更直觀比較哪些組件render更耗時,可以使用React Dev Tools中Profiler面板的火炬圖。
總結
Profiler是React內置的性能分析組件,用于度量其包裹的子樹的渲染性能。
最后說個有意思的細節 —— 在官網Profiler部分[2]中只介紹了Profiler有onRender這個回調。
從Profiler源碼看,他還存在:
- onCommit回調
- onPostCommit回調
- onNestedUpdateScheduled回調
不知道為什么,他們沒有在文檔中提及。
參考資料
[1]why-did-you-render:https://www.npmjs.com/package/@welldone-software/why-did-you-render。
[2]官網Profiler部分:https://react.dev/reference/react/Profiler。