React性能測量和分析

上一篇文章講了 React 性能優化的一些方向和手段,這篇文章再補充說一下如何進行性能測量和分析, 介紹 React 性能分析的一些工具和方法.html

進行任何性能優化的前提是你要找出’性能問題‘,這樣才能針對性地進行優化。我以爲對於 React 的性能優化能夠分兩個階段:react

  • 1. 分析階段git

    • 經過分析器(Profiler)找出從新渲染的組件、從新渲染的次數、以及從新渲染耗費的資源與時間
    • 變更檢測. 經過分析器咱們能夠知道'什麼被從新渲染, 從新渲染的代價',那麼變更檢測回答的問題就是: ’爲何這些進行了從新渲染?'
  • 2. 優化階段. 優化階段咱們針對分析階段拋出的問題進行解決,解決的方法有不少,能夠參考本文的姊妹篇<淺談React性能優化的方向>github


本文大綱web


下面本文測試的樣板代碼.chrome

推薦點擊 Preview 面板的Open In New Window, 或者直接點擊該連接,在線動手實踐數組

Edit React-Performance-Analyze-Demo



分析器

分析哪些組件進行了渲染,以及渲染消耗的時間以及資源。主要工具備 React 官方的開發者工具以及 Chrome 的 Performance 工具。瀏覽器

React Devtool

最早應該使用的確定是官方提供的開發者工具,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 階段。

    • render 階段決定須要進行哪些變動,好比 DOM。顧名思義, 這個階段 React 會調用 render 函數,並將結果和上一次 render 的結果進行 diff, 計算出須要進行變動的操做隊列
    • commit 階段。或者稱爲提交階段, 在這個階段會執行 render 階段 diff 出來的變動請求。好比 DOM 插入、更新、刪除、排序等等。在這個階段 React 還會調用 componentDidMount 和 componentDidUpdate 生命週期函數.

    在 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>



Chrome Performance 工具

在 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 工具備如下優點:

  • 能夠測量分析整個渲染的過程細節. 它能夠定位某些具體方法的調用過程和消耗, 方便定位一些深層次問題.
  • 能夠測量分析底層 DOM 的繪製、佈局、合成等細節。方便定位瀏覽器性能問題

其實 Performance 是一個通用的性能檢測工具,因此其細節不在本文討論訪問。 詳細參考


其餘工具

上面介紹的這些工具基本上已經夠用了。社區上還有一些比較流行的工具,不過這些工具早晚/已經要被官方取代(招安),並且它們也跟不上 React 的更新。




變更檢測

OK, 咱們經過分析工具已經知道咱們的應用存在哪些問題了,診斷出了哪些組件被無心義的渲染。下一步操做就是找出組件從新渲染的元兇, 檢測爲何組件進行了更新.

咱們先假設咱們的組件是一個’純組件‘,也就是說咱們認爲只有組件依賴的狀態變動時,組件纔會從新渲染. 非純組件沒有討論的意義,由於只要狀態變動或父級變動他都會從新渲染。

那麼對於一個’純組件‘來講,通常會有下面這些因素均可能致使組件從新渲染:

  • props + state 毫無疑問. 這裏咱們只須要關注來源於外部的 props. 內部 state 變更通常是人爲觸發的,比較容易發現
  • Mobx observable value. 若是訪問了 mobx 傳進來的響應式數據,就會創建一個狀態依賴關係,這個相對於 props 和 context 來講是隱式的,檢測它的變更咱們可能須要利用 mobx 提供的一些工具
  • Context。 Context 的 value 的變動會強制從新渲染組件

props 變更檢測

在上一篇文章中我就建議簡化 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


mobx 變更檢測

排除了 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 值):

mobx-trace


Context 變動檢測

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 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 />)); 複製代碼


好了行文結束,若是以爲能夠就點個 👍 吧

擴展

相關文章
相關標籤/搜索