上一篇文章講了 React 性能優化的一些方向和手段,這篇文章再補充說一下如何進行性能測量和分析, 介紹 React 性能分析的一些工具和方法.html
進行任何性能優化的前提是你要找出’性能問題‘,這樣才能針對性地進行優化。我以爲對於 React 的性能優化能夠分兩個階段:react
1. 分析階段git
2. 優化階段. 優化階段咱們針對分析階段拋出的問題進行解決,解決的方法有不少,能夠參考本文的姊妹篇<淺談React性能優化的方向>github
本文大綱web
下面本文測試的樣板代碼.chrome
推薦點擊 Preview 面板的
Open In New Window
, 或者直接點擊該連接,在線動手實踐數組
分析哪些組件進行了渲染,以及渲染消耗的時間以及資源。主要工具備 React 官方的開發者工具以及 Chrome 的 Performance 工具。瀏覽器
最早應該使用的確定是官方提供的開發者工具,React v16.5 引入了新的 Profiler 功能,讓分析組件渲染過程變得更加簡單,並且能夠很直觀地查看哪些組件被渲染.性能優化
首先最簡單也是最方便的判斷組件是否被從新渲染的方式是'高亮更新(Hightlight Updates)'.markdown
① 開啓高亮更新:
② 運行效果以下:
③ 經過高亮更新,基本上能夠肯定哪些組件被從新渲染. 因此如今咱們給 ListItem 加上 React.memo(查看 PureList 示例), 看一下效果:
效果很是明顯,如今只有遞增的 ListItem 會被更新,並且當數組排序時只有 List 組件會被刷新. 因此說‘純組件’是 React 優化的第一張牌, 也是最有效的一張牌.
若是高亮更新
沒法知足你的需求,好比你須要知道具體哪些組件被渲染、渲染消耗多少時間、進行了多少次的提交(渲染)等等, 這時候就須要用到分析器了.
① 首先選擇須要收集測量信息的節點(通常默認選中根節點,有一些應用可能存在多個組件樹,這時候須要手動選擇):
② Ok,點擊 Record 開始測量
③ 看看測量的結果,先來了解一下 Profiler 面板的基本結構:
1️⃣ 這是一個 commit 列表。commit 列表表示錄製期間發生的 commit(能夠認爲是渲染) 操做,要理解 commit 的意思還須要瞭解 React 渲染的基本原理.
在 v16 後 React 組件渲染會分爲兩個階段,即 render 和 commit 階段。
在 v16 以前,或者在 Preact 這些'類 React' 框架中,並不區分 render 階段和 commit 階段,也就說這兩個階段糅合在一塊兒,一邊 diff 一邊 commit。有興趣的讀者能夠看筆者以前寫的從 Preact 中瞭解組件和 hooks 基本原理
切換 commit:
2️⃣ 選擇其餘圖形展現形式,例如 Ranked 視圖
,這個視圖按照渲染消耗時間對組件進行排序:
3️⃣ 火焰圖 這個圖其實就是組件樹,Profiler 使用顏色來標記哪些組件被從新渲染。和 commit 列表以及 Ranked 圖同樣,顏色在這裏是有意義的,好比灰色表示沒有從新渲染;從渲染消耗的時間上看的話: 黑色 > 黃色 > 藍色
, 經過 👆Ranked 圖能夠直觀感覺到不一樣顏色之間的意義
4️⃣ 當前選中組件或者 Commit 的詳情, 能夠查看該組件渲染時的 props 和 state
雙擊具體組件能夠詳細比對每一次 commit 消耗的時間:
5️⃣ 設置
另外能夠經過設置,篩選 Commit,以及是否顯示原生元素:
④ 如今使用 Profiler 來分析一下 PureList 的渲染過程:
關於 Profiler 的詳細介紹能夠看這篇官方博客<Introducing the React Profiler>
在 v16.5 以前,咱們通常都是利用 Chrome 自帶的 Performance 來進行 React 性能測量:
React 使用標準的User Timing API
(全部支持該標準的瀏覽器均可以用來分析 React)來記錄操做,因此咱們在 Timings 標籤中查看 React 的渲染過程。React 還特地使用 emoji 標記.
相對 React Devtool 而言 Performance 工具可能還不夠直觀,可是它很是強大,舉個例子,若是說 React-Devtool 是Fiddler, 那麼 Performance 就是Wireshark. 使用 Performance 能夠用來定位一些比較深層次的問題,這可能須要你對 React 的實現原理有必定了解, 就像使用 Wireshark 你須要懂點網絡協議同樣
因此說使用 Performance 工具備如下優點:
其實 Performance 是一個通用的性能檢測工具,因此其細節不在本文討論訪問。 詳細參考
上面介紹的這些工具基本上已經夠用了。社區上還有一些比較流行的工具,不過這些工具早晚/已經要被官方取代(招安),並且它們也跟不上 React 的更新。
OK, 咱們經過分析工具已經知道咱們的應用存在哪些問題了,診斷出了哪些組件被無心義的渲染。下一步操做就是找出組件從新渲染的元兇, 檢測爲何組件進行了更新.
咱們先假設咱們的組件是一個’純組件‘,也就是說咱們認爲只有組件依賴的狀態變動時,組件纔會從新渲染. 非純組件沒有討論的意義,由於只要狀態變動或父級變動他都會從新渲染。
那麼對於一個’純組件‘來講,通常會有下面這些因素均可能致使組件從新渲染:
在上一篇文章中我就建議簡化 props,簡單組件的 props 的變動很容易預測, 甚至你肉眼均可以察覺出來。另外若是你使用 Redux,若是嚴格按照 Redux 的最佳實踐,配合 Redux 的開發者工具,也能夠很直觀地判斷哪些狀態發生了變動。
若是你沒辦法知足以上條件,可能就得依賴工具了。以前有一個why-did-you-update的庫,很惋惜如今已經沒怎麼維護了(舊版本可使用它)。這個庫使用猴補丁(monkey patches)來擴展 React,比對檢測哪些 props 和 state 發生了變化:
後面也有人借鑑 why-did-you-update 寫了個why-did-you-render. 不過筆者仍是不看好這些經過猴補丁擴展 React 的實現,依賴於 React 的內部實現細節,維護成本過高了,跟不上 React 更新基本就廢了.
若是你如今使用 hook 的話,本身手寫一個也很簡單, 這個 idea 來源於use-why-did-you-update:
import { useEffect, useRef } from 'react'; export function useWhyDidYouUpdate(name: string, props: Record<string, any>) { // ⚛️保存上一個props const latestProps = useRef(props); useEffect(() => { if (process.env.NODE_ENV !== 'development') return; const allKeys = Object.keys({ ...latestProps.current, ...props }); const changesObj: Record<string, { from: any; to: any }> = {}; allKeys.forEach(key => { if (latestProps.current[key] !== props[key]) { changesObj[key] = { from: latestProps.current[key], to: props[key] }; } }); if (Object.keys(changesObj).length) { console.log('[why-did-you-update]', name, changesObj); } else { // 其餘緣由致使組件渲染 } latestProps.current = props; }, Object.values(props)); } 複製代碼
使用:
const Counter = React.memo(props => {
useWhyDidYouUpdate('Counter', props);
return <div style={props.style}>{props.count}</div>;
});
複製代碼
若是是類組件,能夠在componentDidUpdate
使用相似上面的方式來比較 props
排除了 props 變動致使的從新渲染,如今來看看是不是 mobx 響應式數據致使的變動. 若是大家團隊不使用 mobx,能夠跳過這一節。
首先不論是 Redux 和 Mobx,咱們都應該讓狀態的變更變得可預測. 由於 Mobx 沒有 Redux 那樣固化的數據變動模式,Mobx 並不容易自動化地監測數據是如何被變動的。在 mobx 中咱們使用@action
來標誌狀態的變動操做,可是它拿異步操做沒辦法。好在後面 mobx 推出了 flow
API👏。
對於 Mobx 首先建議開啓嚴格模式, 要求全部數據變動都放在@action 或 flow 中:
import { configure } from 'mobx'; configure({ enforceActions: 'always' }); 複製代碼
定義狀態變動操做
import { observable, action, flow } from 'mobx'; class CounterStore { @observable count = 0; // 同步操做 @action('increment count') increment = () => { this.count++; }; // 異步操做 // 這是一個生成器,相似於saga的機制 fetchCount = flow(function*() { const count = yield getCount(); this.count = count; }); } 複製代碼
Ok 有了上面的約定,如今能夠在控制檯(經過 mobx-logger)或者 Mobx 開發者工具中跟蹤 Mobx 響應式數據的變更了。
若是不按照規範來,出現問題會比較浪費時間, 但也不是沒辦法解決。Mobx 還提供了一個trace函數, 用來檢測爲何會執行 SideEffect:
export const ListItem = observer(props => { const { item, onShiftDown } = props; trace(); return <div className="list-item">{/*...*/}</div>; }); 複製代碼
運行效果(遞增了 value 值):
Ok, 若是排除了 props 和 mobx 數據變動還會從新渲染,那麼 100%是 Context 致使的,由於一旦 Context 數據變更,組件就會被強制渲染。筆者在淺談 React 性能優化的方向提到了 ContextAPI 的一些陷阱。先排除一下是不是這些緣由致使的.
如今並無合適的跟蹤 context 變更的機制,咱們能夠採起像上文的useWhyDidYouUpdate
同樣的方式來比對 Context 的值:
function useIsContextUpdate(contexts: object = {}) { const latestContexts = useRef(contexts); useEffect(() => { if (process.env.NODE_ENV !== 'development') return; const changedContexts: string[] = []; for (const key in contexts) { if (contexts[key] !== latestContexts.current[key]) { changedContexts.push(key); } } if (changedContexts.length) { console.log(`[is-context-update]: ${changedContexts.join(', ')}`); } latestContexts.current = contexts; }); } 複製代碼
用法:
const router = useRouter(); const myContext = useContext(MyContext); useIsContextUpdate({ router, myContext, }); 複製代碼
這是 React Devtool 的一個實驗性功能,Interactions 翻譯爲中文是‘交互’?這個東西目的其實就是爲了跟蹤‘什麼致使了更新’,也就是咱們上面說的變更檢測。React但願提供一個通用的API給開發者或第三方工具,方便開發者直觀地定位更新的緣由:
上圖表示在記錄期間跟蹤到了四個交互,以及交互觸發的時間和耗時。由於仍是一個Idea階段,因此咱們就挑選一些API代碼隨便看看:
/** 跟蹤狀態變動 **/ import { unstable_trace as trace } from "scheduler/tracing"; class MyComponent extends Component { handleLoginButtonClick = event => { // 跟蹤setState trace("Login button click", performance.now(), () => { this.setState({ isLoggingIn: true }); }); }; // render ... } /** 跟蹤異步操做 **/ import { unstable_trace as trace, unstable_wrap as wrap } from "scheduler/tracing"; trace("Some event", performance.now(), () => { setTimeout( wrap(() => { // Do some async work }) ); }); /** 跟蹤初始化渲染 **/ trace("initial render", performance.now(), () => render(<Application />)); 複製代碼