從源碼中來,到業務中去,React性能優化終極指南

前言:咱們從React源碼入手,結合有道精品課大前端的具體業務,運用三大原則對系統進行外科手術式的優化。同時介紹React Profiler這款工具如何幫咱們定位性能瓶頸前言:咱們從React源碼入手,結合有道精品課大前端的具體業務,運用三大原則對系統進行外科手術式的優化。同時介紹React Profiler這款工具如何幫咱們定位性能瓶頸css

做者/ 安增平html

編輯/ Ein前端

React性能優化是在業務迭代過程當中不得不考慮的問題,大部分狀況是因爲項目啓動之初,沒有充分考慮到項目的複雜度,定位該產品的用戶體量及技術場景並不複雜,那麼咱們在業務前期可能並不須要考慮性能優化。可是隨着業務場景的複雜化,性能優化就變得格外重要。react

咱們從React源碼入手,結合有道精品課大前端的具體業務,運用優化技巧對系統進行外科手術式的優化。同時介紹一下React Profiler這款性能優化的利器是如何幫咱們定位性能瓶頸的。web

本文中的項目代碼所有是在有道大前端組開發項目中的工做記錄,若有不足歡迎在留言區討論交流,筆芯❤算法

頁面加載流程

  1. 假設用戶首次打開頁面(無緩存),這個時候頁面是徹底空白的;
  2. html 和引用的 css 加載完畢,瀏覽器進行首次渲染
  3. react、react-dom、業務代碼加載完畢,應用第一次渲染,或者說首次內容渲染
  4. 應用的代碼開始執行,拉取數據、進行動態import、響應事件等等,完畢後頁面進入可交互狀態;
  5. 接下來 lazyload 的圖片等多媒體內容開始逐漸加載完畢;
  6. 直到頁面的其它資源(如錯誤上報組件、打點上報組件等)加載完畢,整個頁面加載完成。

咱們主要來針對React進行剖析spring

React 針對渲染性能優化的三個方向,也適用於其餘軟件開發領域,這三個方向分別是:redux

  1. 減小計算的量:React 中就是減小渲染的節點或經過索引減小渲染複雜度;
  2. 利用緩存:React 中就是避免從新渲染(利用 memo 方式來避免組件從新渲染);
  3. 精確從新計算的範圍:React 中就是綁定組件和狀態關係, 精確判斷更新的'時機'和'範圍'. 只從新渲染變動的組件(減小渲染範圍)。

如何作到這三點呢?咱們從React自己的特性入手分析。數組

React 工做流

React 是聲明式 UI 庫,負責將 State 轉換爲頁面結構(虛擬 DOM 結構)後,再轉換成真實 DOM 結構,交給瀏覽器渲染。State 發生改變時,React 會先進行Reconciliation,結束後馬上進入Commit階段,Commit結束後,新 State 對應的頁面才被展現出來。瀏覽器

React 的Reconciliation須要作兩件事:

  1. 計算出目標 State 對應的虛擬 DOM 結構。
  2. 尋找「將虛擬 DOM 結構修改成目標虛擬 DOM 結構」的最優方案。

React 按照深度優先遍歷虛擬 DOM 樹的方式,在一個虛擬 DOM 上完成Render和Diff的計算後,再計算下一個虛擬 DOM。Diff 算法會記錄虛擬 DOM 的更新方式(如:Update、Mount、Unmount),爲Commit作準備。

React 的Commit也須要作兩件事:

  1. 將Reconciliation結果應用到 DOM 中。
  2. 調用暴露的hooks如:componentDidUpdate、useLayoutEffect 等。

下面咱們將針對三個優化方向進行精準分析。

減小計算的量

關於以上ReconciliationCommit兩個階段的優化辦法,我在實現的過程當中遵循減小計算量的方法進行優化(列表項使用 key 屬性)該過程是優化的重點,React 內部的 Fiber 結構和併發模式也是在減小該過程的耗時阻塞。對於Commit在執行hooks時,開發者應保證hooks中的代碼儘可能輕量,避免耗時阻塞,同時應避免在 CDM、CDU週期中更新組件。

列表項使用 key 屬性

特定框架中,提示也作的十分友好。假如你沒有在列表中添加key屬性,控制檯會爲你展現一片大紅

系統會時刻提醒你記得加Key哦~~

優化Render 過程

Render 過程:即Reconciliation中計算出目標 State 對應的虛擬 DOM 結構這一階段 。

觸發 React 組件的 Render 過程目前有三種方式:

  1. forceUpdate、
  2. State 更新、
  3. 父組件 Render 觸發子組件 Render 過程。

優化技巧

PureComponent、React.memo

在 React 工做流中,若是隻有父組件發生狀態更新,即便父組件傳給子組件的全部 Props 都沒有修改,也會引發子組件的 Render 過程。

從 React 的聲明式設計理念來看,若是子組件的 Props 和 State 都沒有改變,那麼其生成的 DOM 結構和反作用也不該該發生改變。當子組件符合聲明式設計理念時,就能夠忽略子組件本次的 Render 過程。

PureComponent 和 React.memo 就是應對這種場景的,PureComponent 是對類組件的 Props 和 State 進行淺比較,React.memo 是對函數組件的 Props 進行淺比較。

useMemo、useCallback 實現穩定的 Props 值

若是傳給子組件的派生狀態或函數,每次都是新的引用,那麼 PureComponent 和 React.memo 優化就會失效。因此須要使用 useMemo 和 useCallback 來生成穩定值,並結合 PureComponent 或 React.memo 避免子組件從新 Render。

useMemo 減小組件 Render 過程耗時

useMemo 是一種緩存機制提速,當它的依賴未發生改變時,就不會觸發從新計算。通常用在「計算派生狀態的代碼」很是耗時的場景中,如:遍歷大列表作統計信息。

大列表渲染

顯然useMemo的做用是緩存昂貴的計算(避免在每次渲染時都進行高開銷的計算),在業務中使用它去控制變量來更新表格

shouldComponentUpdate

在類組件中,例如要往數組中添加一項數據時,當時的代碼極可能是 state.push(item),而不是 const newState = [...state, item]。

在此背景下,當時的開發者常用

shouldComponentUpdate 來深比較 Props,只在 Props 有修改才執行組件的 Render 過程。現在因爲數據不可變性和函數組件的流行,這樣的優化場景已經不會再出現了。

爲了貼合shouldComponentUpdate的思想:給子組件傳props的時候必定只傳其須要的而並不是一股腦所有傳入:

傳入到子組件的參數必定保證其在自組件中被使用到。

批量更新,減小 Render 次數

在 React 管理的事件回調和生命週期中,setState 是異步的,而其餘時候 setState 都是同步的。這個問題根本緣由就是 React 在本身管理的事件回調和生命週期中,對於 setState 是批量更新的,而在其餘時候是當即更新的。

批量更新 setState 時,屢次執行 setState 只會觸發一次 Render 過程。相反在當即更新 setState 時,每次 setState 都會觸發一次 Render 過程,就存在性能影響。

假設有以下組件代碼,該組件在 getData() 的 API 請求結果返回後,分別更新了兩個 State 。

該組件會在 setList(data.list) 後觸發組件的 Render 過程,而後在 setInfo(data.info) 後再次觸發 Render 過程,形成性能損失。那咱們該如何解決呢:

  1. 將多個 State 合併爲單個 State。例如 useState({ list: null, info: null }) 替代 list 和 info 兩個 State。
  2. 使用 React 官方提供的 unstable_batchedUpdates 方法,將屢次 setState 封裝到 unstable_batchedUpdates 回調中。

修改後代碼以下:

精細化渲染階段

按優先級更新,及時響應用戶

優先級更新是批量更新的逆向操做,其思想是:優先響應用戶行爲,再完成耗時操做。常見的場景是:頁面彈出一個 Modal,當用戶點擊 Modal 中的肯定按鈕後,代碼將執行兩個操做:

  1. 關閉 Modal。
  2. 頁面處理 Modal 傳回的數據並展現給用戶。

當操做2須要執行500ms時,用戶會明顯感受到從點擊按鈕到 Modal 被關閉之間的延遲。

如下爲通常的實現方式,將 slowHandle 函數做爲用戶點擊按鈕的回調函數。

slowHandle() 執行過程耗時長,用戶點擊按鈕後會明顯感受到頁面卡頓。

若是讓頁面優先隱藏輸入框,用戶便能馬上感知到頁面更新,不會有卡頓感。

實現優先級更新的要點是將耗時任務移動到下一個宏任務中執行,優先響應用戶行爲。

例如在該例中,將 setNumbers 移動到 setTimeout 的回調中,用戶點擊按鈕後便能當即看到輸入框被隱藏,不會感知到頁面卡頓。mhd項目中優化後的代碼以下:

發佈者訂閱者跳過中間組件 Render 過程

React 推薦將公共數據放在全部「須要該狀態的組件」的公共祖先上,但將狀態放在公共祖先上後,該狀態就須要層層向下傳遞,直到傳遞給使用該狀態的組件爲止。

每次狀態的更新都會涉及中間組件的 Render 過程,但中間組件並不關心該狀態,它的 Render 過程只負責將該狀態再傳給子組件。在這種場景下能夠將狀態用發佈者訂閱者模式維護,只有關心該狀態的組件纔去訂閱該狀態,再也不須要中間組件傳遞該狀態。

當狀態更新時,發佈者發佈數據更新消息,只有訂閱者組件纔會觸發 Render 過程,中間組件再也不執行 Render 過程。

只要是發佈者訂閱者模式的庫,均可以使用useContext進行該優化。好比:redux、use-global-state、React.createContext 等。

業務代碼中的使用以下:

從圖中可看出,優化後只有使用了公共狀態的組件renderTable纔會發生更新,因而可知這樣作能夠大大減小父組件和 其餘renderSon... 組件的 Render 次數(減小葉子節點的重渲染)。

useMemo 返回虛擬 DOM 可跳過該組件 Render 過程

利用 useMemo 能夠緩存計算結果的特色,若是 useMemo 返回的是組件的虛擬 DOM,則將在 useMemo 依賴不變時,跳過組件的 Render 階段。

該方式與 React.memo 相似,但與 React.memo 相比有如下優點:

  1. 更方便。React.memo 須要對組件進行一次包裝,生成新的組件。而 useMemo 只需在存在性能瓶頸的地方使用,不用修改組件。
  2. 更靈活。useMemo 不用考慮組件的全部 Props,而只需考慮當前場景中用到的值,也可以使用 useDeepCompareMemo 對用到的值進行深比較。

該例子中,父組件狀態更新後,不使用 useMemo 的子組件會執行 Render 過程,而使用 useMemo 的子組件會按需執行更新。業務代碼中的使用方法:

精確判斷更新的'時機'和'範圍'

debounce、throttle 優化頻繁觸發的回調

在搜索組件中,當 input 中內容修改時就觸發搜索回調。當組件能很快處理搜索結果時,用戶不會感受到輸入延遲。

但實際場景中,中後臺應用的列表頁很是複雜,組件對搜索結果的 Render 會形成頁面卡頓,明顯影響到用戶的輸入體驗。

在搜索場景中通常使用 useDebounce+ useEffect 的方式獲取數據。

在搜索場景中,只需響應用戶最後一次輸入,無需響應用戶的中間輸入值,debounce 更適合。而 throttle 更適合須要實時響應用戶的場景中更適合,如經過拖拽調整尺寸或經過拖拽進行放大縮小(如:window 的 resize 事件)。

懶加載

在 SPA 中,懶加載優化通常用於從一個路由跳轉到另外一個路由。

還可用於用戶操做後才展現的複雜組件,好比點擊按鈕後展現的彈窗模塊(大數據量彈窗)。

在這些場景下,結合 Code Split 收益較高。懶加載的實現是經過 Webpack 的動態導入和 React.lazy 方法。

實現懶加載優化時,不只要考慮加載態,還須要對加載失敗進行容錯處理。

懶渲染

懶渲染指當組件進入或即將進入可視區域時才渲染組件。常見的組件 Modal/Drawer 等,當 visible 屬性爲 true 時才渲染組件內容,也能夠認爲是懶渲染的一種實現。懶渲染的使用場景有:

  1. 頁面中出現屢次的組件,且組件渲染費時、或者組件中含有接口請求。若是渲染多個帶有請求的組件,因爲瀏覽器限制了同域名下併發請求的數量,就可能會阻塞可見區域內的其餘組件中的請求,致使可見區域的內容被延遲展現。
  2. 需用戶操做後才展現的組件。這點和懶加載同樣,但懶渲染不用動態加載模塊,不用考慮加載態和加載失敗的兜底處理,實現上更簡單。

懶渲染的實現中判斷組件是否出如今可視區域內藉助react-visibility-observer依賴:

虛擬列表

虛擬列表是懶渲染的一種特殊場景。虛擬列表的組件有 react-window和 react-virtualized,它們都是同一個做者開發的。

react-window 是 react-virtualized 的輕量版本,其 API 和文檔更加友好。推薦使用 react-window,只須要計算每項的高度便可:

若是每項的高度是變化的,可給 itemSize 參數傳一個函數。

因此在開發過程當中,遇到接口返回的是全部數據時,需提早預防這類會有展現的性能瓶頸的需求時,推薦使用虛擬列表優化。使用示例:react-window​react-window.vercel.app

動畫庫直接修改 DOM 屬性,跳過組件 Render 階段

這個優化在業務中應該用不上,但仍是很是值得學習的,未來能夠應用到組件庫中。

參考 react-spring 的動畫實現,當一個動畫啓動後,每次動畫屬性改變不會引發組件從新 Render ,而是直接修改了 dom 上相關屬性值:

避免在 didMount、didUpdate 中更新組件 State

這個技巧不只僅適用於 didMount、didUpdate,還包括 willUnmount、useLayoutEffect 和特殊場景下的 useEffect(當父組件的 cDU/cDM 觸發時,子組件的 useEffect 會同步調用),本文爲敘述方便將他們統稱爲「提交階段鉤子」。

React 工做流commit階段的第二步就是執行提交階段鉤子,它們的執行會阻塞瀏覽器更新頁面。

若是在提交階段鉤子函數中更新組件 State,會再次觸發組件的更新流程,形成兩倍耗時。通常在提交階段的鉤子中更新組件狀態的場景有:

  1. 計算並更新組件的派生狀態(Derived State)。在該場景中,類組件應使用 getDerivedStateFromProps 鉤子方法代替,函數組件應使用函數調用時執行 setState 的方式代替。使用上面兩種方式後,React 會將新狀態和派生狀態在一次更新內完成。
  2. 根據 DOM 信息,修改組件狀態。在該場景中,除非想辦法不依賴 DOM 信息,不然兩次更新過程是少不了的,就只能用其餘優化技巧了。

use-swr 的源碼就使用了該優化技巧。當某個接口存在緩存數據時,use-swr 會先使用該接口的緩存數據,並在 requestIdleCallback 時再從新發起請求,獲取最新數據。模擬一個swr:

  1. 它的第二個參數 deps,是爲了在請求帶有參數時,若是參數改變了就從新發起請求。
  2. 暴露給調用方的 fetch 函數,能夠應對主動刷新的場景,好比頁面上的刷新按鈕。

若是 use-swr 不作該優化的話,就會在 useLayoutEffect 中觸發從新驗證並設置 isValidating 狀態爲 true·,引發組件的更新流程,形成性能損失。

工具介紹——React Profiler

React Profiler 定位 Render 過程瓶頸

React Profiler 是 React 官方提供的性能審查工具,本文只介紹筆者的使用心得,詳細的使用手冊請移步官網文檔。

Note:react-dom 16.5+ 在 DEV 模式下才支持 Profiling,同時生產環境下也能夠經過一個 profiling bundle react-dom/profiling 來支持。請在 fb.me/react-profi… 上查看如何使用這個 bundle。

「Profiler」 的面板在剛開始的時候是空的。你能夠點擊 record 按鈕來啓動 profile:

Profiler 只記錄了 Render 過程耗時

不要經過 Profiler 定位非 Render 過程的性能瓶頸問題

經過 React Profiler,開發者能夠查看組件 Render 過程耗時,但沒法知曉提交階段的耗時。

儘管 Profiler 面板中有 Committed at 字段,但這個字段是相對於錄製開始時間,根本沒有意義。

經過在 React v16 版本上進行實驗,同時開啓 Chrome 的 Performance 和 React Profiler 統計。

以下圖,在 Performance 面板中,Reconciliation和Commit階段耗時分別爲 642ms 和 300ms,而 Profiler 面板中只顯示了 642ms:

開啓「記錄組件更新緣由」

點擊面板上的齒輪,而後勾選「Record why each component rendered while profiling.」,以下圖:

而後點擊面板中的虛擬 DOM 節點,右側便會展現該組件從新 Render 的緣由。

定位產生本次 Render 過程緣由

因爲 React 的批量更新(Batch Update)機制,產生一次 Render 過程可能涉及到不少個組件的狀態更新。那麼如何定位是哪些組件狀態更新致使的呢?

在 Profiler 面板左側的虛擬 DOM 樹結構中,從上到下審查每一個發生了渲染的(不會灰色的)組件。

若是組件是因爲 State 或 Hook 改變觸發了 Render 過程,那它就是咱們要找的組件,以下圖:

站在巨人的肩膀上

Optimizing Performance React 官方文檔,最好的教程, 利用好 React 的性能分析工具。

Twitter Lite and High Performance React Progressive Web Apps at Scale 看看 Twitter 如何優化的。

-END-

相關文章
相關標籤/搜索