日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯(lián)網營銷解決方案
React 性能優(yōu)化 :包括原理、技巧、Demo、工具使用

 本文分為三部分,首先介紹 React 的工作流,讓讀者對 React 組件更新流程有宏觀的認識。然后列出筆者總結的一系列優(yōu)化技巧,并為稍復雜的優(yōu)化技巧準備了 CodeSandbox 源碼,以便讀者實操體驗。最后分享筆者使用 React Profiler 的一點心得,幫助讀者更快定位性能瓶頸。

清水河ssl適用于網站、小程序/APP、API接口等需要進行數(shù)據(jù)傳輸應用場景,ssl證書未來市場廣闊!成為創(chuàng)新互聯(lián)的ssl證書銷售渠道,可以享受市場價格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:13518219792(備注:SSL證書合作)期待與您的合作!

React 工作流

React 是聲明式 UI 庫,負責將 State 轉換為頁面結構(虛擬 DOM 結構)后,再轉換成真實 DOM 結構,交給瀏覽器渲染。當 State 發(fā)生改變時,React 會先進行調和(Reconciliation)階段,調和階段結束后立刻進入提交(Commit)階段,提交階段結束后,新 State 對應的頁面才被展示出來。

React 的調和階段需要做兩件事。1、計算出目標 State 對應的虛擬 DOM 結構。2、尋找「將虛擬 DOM 結構修改為目標虛擬 DOM 結構」的最優(yōu)更新方案。 React 按照深度優(yōu)先遍歷虛擬 DOM 樹的方式,在一個虛擬 DOM 上完成兩件事的計算后,再計算下一個虛擬 DOM。第一件事主要是調用類組件的 render 方法或函數(shù)組件自身。第二件事為 React 內部實現(xiàn)的 Diff 算法,Diff 算法會記錄虛擬 DOM 的更新方式(如:Update、Mount、Unmount),為提交階段做準備。

React 的提交階段也需要做兩件事。1、將調和階段記錄的更新方案應用到 DOM 中。2、調用暴露給開發(fā)者的鉤子方法,如:componentDidUpdate、useLayoutEffect 等。 提交階段中這兩件事的執(zhí)行時機與調和階段不同,在提交階段 React 會先執(zhí)行 1,等 1 完成后再執(zhí)行 2。因此在子組件的 componentDidMount 方法中,可以執(zhí)行 document.querySelector('.parentClass') ,拿到父組件渲染的 .parentClass DOM 節(jié)點,盡管這時候父組件的 componentDidMount 方法還沒有被執(zhí)行。useLayoutEffect 的執(zhí)行時機與 componentDidMount 相同,可參考線上代碼進行驗證。

由于調和階段的「Diff 過程」和提交階段的「應用更新方案到 DOM」都屬于 React 的內部實現(xiàn),開發(fā)者能提供的優(yōu)化能力有限,本文僅有一條優(yōu)化技巧(列表項使用 key 屬性[1])與它們有關。實際工程中大部分優(yōu)化方式都集中在調和階段的「計算目標虛擬 DOM 結構」過程,該過程是優(yōu)化的重點,React 內部的 Fiber 架構和并發(fā)模式也是在減少該過程的耗時阻塞。對于提交階段的「執(zhí)行鉤子函數(shù)」過程,開發(fā)者應保證鉤子函數(shù)中的代碼盡量輕量,避免耗時阻塞,相關的優(yōu)化技巧參考本文的避免在 didMount、didUpdate 中更新組件 State[2]。

拓展知識

  1.   建議對 React 生命周期不熟悉的讀者結合 React 組件的生命周期圖[3]閱讀本文。記得勾選該網站上的復選框。
  2.   因為理解事件循環(huán)后才知道頁面會在什么時候被更新,所以推薦一個介紹事件循環(huán)的視頻[4]。該視頻中事件循環(huán)的偽代碼如下圖,非常清晰易懂。

定義 Render 過程

本文為了敘述方便, 將調和階段中「計算目標虛擬 DOM 結構」過程稱為 Render 過程 。觸發(fā) React 組件的 Render 過程目前有三種方式,分別為 forceUpdate、State 更新、父組件 Render 觸發(fā)子組件 Render 過程。

優(yōu)化技巧

本文將優(yōu)化技巧分為三大類,分別為:

  1.  跳過不必要的組件更新。這類優(yōu)化是在組件狀態(tài)發(fā)生變更后,通過減少不必要的組件更新來實現(xiàn),是本文優(yōu)化技巧的主要部分。
  2.  提交階段優(yōu)化。這類優(yōu)化的目的是減少提交階段耗時,該分類中僅有一條優(yōu)化技巧。
  3.  前端通用優(yōu)化。這類優(yōu)化在所有前端框架中都存在,本文的重點就在于將這些技巧應用在 React 組件中。

跳過不必要的組件更新

這類優(yōu)化是在組件狀態(tài)發(fā)生變更后,通過減少不必要的組件更新來實現(xiàn),是本文優(yōu)化技巧的主要部分。

1. PureComponent、React.memo

在 React 工作流中,如果只有父組件發(fā)生狀態(tài)更新,即使父組件傳給子組件的所有 Props 都沒有修改,也會引起子組件的 Render 過程。從 React 的聲明式設計理念來看,如果子組件的 Props 和 State 都沒有改變,那么其生成的 DOM 結構和副作用也不應該發(fā)生改變。當子組件符合聲明式設計理念時,就可以忽略子組件本次的 Render 過程。PureComponent 和 React.memo 就是應對這種場景的,PureComponent 是對類組件的 Props 和 State 進行淺比較,React.memo 是對函數(shù)組件的 Props 進行淺比較。

2. shouldComponentUpdate

在 React 剛開源的那段時期,數(shù)據(jù)不可變性還沒有現(xiàn)在這樣流行。當時 Flux 架構就使用的模塊變量來維護 State,并在狀態(tài)更新時直接修改該模塊變量的屬性值,而不是使用展開語法[5]生成新的對象引用。例如要往數(shù)組中添加一項數(shù)據(jù)時,當時的代碼很可能是 state.push(item),而不是 const newState = [...state, item]。這點可參考 Dan Abramov 在演講 Redux 時[6]演示的 Flux 代碼。

在此背景下,當時的開發(fā)者經常使用 shouldComponentUpdate 來深比較 Props,只在 Props 有修改才執(zhí)行組件的 Render 過程。如今由于數(shù)據(jù)不可變性和函數(shù)組件的流行,這樣的優(yōu)化場景已經不會再出現(xiàn)了。

接下來介紹另一種可以使用 shouldComponentUpdate 來優(yōu)化的場景。在項目初始階段,開發(fā)者往往圖方便會給子組件傳遞一個大對象作為 Props,后面子組件想用啥就用啥。當大對象中某個「子組件未使用的屬性」發(fā)生了更新,子組件也會觸發(fā) Render 過程。在這種場景下,通過實現(xiàn)子組件的 shouldComponentUpdate 方法,僅在「子組件使用的屬性」發(fā)生改變時才返回 true,便能避免子組件重新 Render。

但使用 shouldComponentUpdate 優(yōu)化第二個場景有兩個弊端。

  1.  如果存在很多子孫組件,「找出所有子孫組件使用的屬性」就會有很多工作量,也容易因為漏測導致 bug。
  2.  存在潛在的工程隱患。舉例來說,假設組件結構如下。 
 
 
 
 
  1.   
  2.   {/* B 組件只使用了 data.a 和 data.b */}  
  3.     
  4.     {/* C 組件只使用了 data.a */}  
  5.       
  6.     
  7.   
  8. 復制代碼 

B 組件的 shouldComponentUpdate 中只比較了 data.a 和 data.b,目前是沒任何問題的。之后開發(fā)者想在 C 組件中使用 data.c,假設項目中 data.a 和 data.c 是一起更新的,所以也沒任何問題。但這份代碼已經變得脆弱了,如果某次修改導致 data.a 和 data.c 不一起更新了,那么系統(tǒng)就會出問題。而且實際業(yè)務中代碼往往更復雜,從 B 到 C 可能還有若干中間組件,這時就很難想到是 shouldComponentUpdate 引起的問題了。

拓展知識

       1.   第二個場景最好的解決方案是使用發(fā)布者訂閱者模式,只是代碼改動要稍多一些,可參考本文的優(yōu)化技巧「發(fā)布者訂閱者跳過中間組件 Render 過程[7]」。

        2.  第二個場景也可以在父子組件間增加中間組件,中間組件負責從父組件中選出子組件關心的屬性,再傳給子組件。相比于 shouldComponentUpdate 方法,會增加組件層級,但不會有第二個弊端。

        3.  本文中的跳過回調函數(shù)改變觸發(fā)的 Render 過程[8]也可以用 shouldComponentUpdate 實現(xiàn),因為回調函數(shù)并不參與組件的 Render 過程。

3. useMemo、useCallback 實現(xiàn)穩(wěn)定的 Props 值

如果傳給子組件的派生狀態(tài)或函數(shù),每次都是新的引用,那么 PureComponent 和 React.memo 優(yōu)化就會失效。所以需要使用 useMemo 和 useCallback 來生成穩(wěn)定值,并結合 PureComponent 或 React.memo 避免子組件重新 Render。

拓展知識

useCallback 是「useMemo 的返回值為函數(shù)」時的特殊情況,是 React 提供的便捷方式。在 React Server Hooks 代碼[9] 中,useCallback 就是基于 useMemo 實現(xiàn)的。盡管 React Client Hooks 沒有使用同一份代碼,但 useCallback[10] 的代碼邏輯和 useMemo[11] 的代碼邏輯仍是一樣的。

4. 發(fā)布者訂閱者跳過中間組件 Render 過程

React 推薦將公共數(shù)據(jù)放在所有「需要該狀態(tài)的組件」的公共祖先上,但將狀態(tài)放在公共祖先上后,該狀態(tài)就需要層層向下傳遞,直到傳遞給使用該狀態(tài)的組件為止。

每次狀態(tài)的更新都會涉及中間組件的 Render 過程,但中間組件并不關心該狀態(tài),它的 Render 過程只負責將該狀態(tài)再傳給子組件。在這種場景下可以將狀態(tài)用發(fā)布者訂閱者模式維護,只有關心該狀態(tài)的組件才去訂閱該狀態(tài),不再需要中間組件傳遞該狀態(tài)。當狀態(tài)更新時,發(fā)布者發(fā)布數(shù)據(jù)更新消息,只有訂閱者組件才會觸發(fā) Render 過程,中間組件不再執(zhí)行 Render 過程。

只要是發(fā)布者訂閱者模式的庫,都可以進行該優(yōu)化。比如:redux、use-global-state、React.createContext 等。例子參考:發(fā)布者訂閱者模式跳過中間組件的渲染階段[12],本示例使用 React.createContext 進行實現(xiàn)。

 
 
 
 
  1. import { useState, useEffect, createContext, useContext } from "react"  
  2. const renderCntMap = {}  
  3. const renderOnce = name => {  
  4.   return (renderCntMap[name] = (renderCntMap[name] || 0) + 1)  
  5. }  
  6. // 將需要公共訪問的部分移動到 Context 中進行優(yōu)化  
  7. // Context.Provider 就是發(fā)布者  
  8. // Context.Consumer 就是消費者  
  9. const ValueCtx = createContext()  
  10. const CtxContainer = ({ children }) => {  
  11.   const [cnt, setCnt] = useState(0)  
  12.   useEffect(() => {  
  13.     const timer = window.setInterval(() => {  
  14.       setCnt(v => v + 1)  
  15.     }, 1000)  
  16.     return () => clearInterval(timer)  
  17.   }, [setCnt])  
  18.   return {children}  
  19. }  
  20. function CompA({}) {  
  21.   const cnt = useContext(ValueCtx)  
  22.   // 組件內使用 cnt  
  23.   return 
    組件 CompA Render 次數(shù):{renderOnce("CompA")}
      
  24. }  
  25. function CompB({}) {  
  26.   const cnt = useContext(ValueCtx)  
  27.   // 組件內使用 cnt  
  28.   return 
    組件 CompB Render 次數(shù):{renderOnce("CompB")}
      
  29. }  
  30. function CompC({}) {  
  31.   return 
    組件 CompC Render 次數(shù):{renderOnce("CompC")}
     
  32. }  
  33. export const PubSubCommunicate = () => {  
  34.   return (  
  35.       
  36.       
      
  37.         

    優(yōu)化后場景

      
  38.         
      
  39.           將狀態(tài)提升至最低公共祖先的上層,用 CtxContainer 將其內容包裹。  
  40.         
  
  •           
  •           每次 Render 時,只有組件A和組件B會重新 Render 。  
  •         
  •   
  •           
  •           父組件 Render 次數(shù):{renderOnce("parent")}  
  •         
  •   
  •           
  •           
  •           
  •       
  •   
  •       
  •   )  
  • }   
  • export default PubSubCommunicate  
  • 復制代碼 
  • 5. 狀態(tài)下放,縮小狀態(tài)影響范圍

    如果一個狀態(tài)只在某部分子樹中使用,那么可以將這部分子樹提取為組件,并將該狀態(tài)移動到該組件內部。如下面的代碼所示,雖然狀態(tài) color 只在

    中使用,但 color 改變會引起 重新 Render。

     
     
     
     
    1. import { useState } from "react"  
    2. export default function App() {  
    3.   let [color, setColor] = useState("red")  
    4.   return (  
    5.     
        
    6.        setColor(e.target.value)} />  
    7.       Hello, world!

        
    8.         
    9.     
      
  •   )  
  • }   
  • function ExpensiveTree() {  
  •   let now = performance.now()  
  •   while (performance.now() - now < 100) {  
  •     // Artificial delay -- do nothing for 100ms  
  •   }  
  •   return 

    I am a very slow component tree.

      
  • }  
  • 復制代碼 
  • 通過將 color 狀態(tài)、

    提取到組件 Form 中,結果如下。

     
     
     
     
    1. export default function App() {  
    2.   return (  
    3.     <>  
    4.         
    5.         
    6.       
    7.   )  
    8. }  
    9. function Form() {  
    10.   let [color, setColor] = useState("red")  
    11.   return (  
    12.     <>  
    13.        setColor(e.target.value)} />  
    14.       Hello, world!

        
    15.       
    16.   )  
    17. }  
    18. 復制代碼 

    這樣調整之后,color 改變就不會引起組件 App 和 ExpensiveTree 重新 Render 了。

    如果對上面的場景進行擴展,在組件 App 的頂層和子樹中都使用了狀態(tài) color ,但 仍然不關心它,如下所示。

     
     
     
     
    1. import { useState } from "react"  
    2. export default function App() {  
    3.   let [color, setColor] = useState("red")  
    4.   return (  
    5.      
    6.        setColor(e.target.value)} />  
    7.         
    8.       Hello, world!

        
    9.     
      
  •   )  
  • }  
  • 復制代碼 
  • 在這種場景中,我們仍然將 color 狀態(tài)抽取到新組件中,并提供一個插槽來組合 ,如下所示。

     
     
     
     
    1. import { useState } from "react"  
    2. export default function App() {  
    3.   return }>  
    4. }  
    5. function ColorContainer({ expensiveTreeNode }) {  
    6.   let [color, setColor] = useState("red")  
    7.   return (  
    8.       
    9.        setColor(e.target.value)} />  
    10.       {expensiveTreeNode}  
    11.       Hello, world!

        
    12.     
      
  •   )  
  • }  
  • 復制代碼 
  • 這樣調整之后,color 改變就不會引起組件 App 和 ExpensiveTree 重新 Render 了。

    該優(yōu)化技巧來源于 before-you-memo[13],Dan 認為這種優(yōu)化方式在 Server Component 場景下更有效,因為 可以在服務端執(zhí)行。

    6. 列表項使用 key 屬性

    當渲染列表項時,如果不給組件設置不相等的屬性 key,就會收到如下報警。

    相信很多開發(fā)者已經見過該報警成百上千次了,那 key 屬性到底在優(yōu)化了什么呢?舉個 ,在不使用 key 時,組件兩次 Render 的結果如下。

     
     
     
     
    1.   
      •   
      •   
      • Duke
      •   
      •   
      • Villanova
      •   
        
    2.   
      •   
      •   
      • Connecticut
      •   
      •   
      • Duke
      •   
      •   
      • Villanova
      •   
        
    3. 復制代碼 

    此時 React 的 Diff 算法會按照

  • 出現(xiàn)的先后順序進行比較,得出結果為需要更新前兩個
  • 并創(chuàng)建內容為 Villanova 的li,一共會執(zhí)行兩次 DOM 更新、一次 DOM 創(chuàng)建。

    如果加上 React 的 key 屬性,兩次 Render 結果如下。

     
     
     
     
    1.   
      •   
      •   Duke
      •   
      •   Villanova
      •   
        
    2.   
      •   
      •   Connecticut
      •   
      •   Duke
      •   
      •   Villanova
      •  
        
    3. 復制代碼 

    React Diff 算法會把 key 值為 2015 的虛擬 DOM 進行比較,發(fā)現(xiàn) key 為 2015 的虛擬 DOM 沒有發(fā)生修改,不用更新。同樣,key 值為 2016 的虛擬 DOM 也不需要更新。結果就只需要創(chuàng)建 key 值為 2014 的虛擬 DOM。相比于不使用 key 的代碼,使用 key 節(jié)省了兩次 DOM 更新操作。

    如果把例子中的

  • 換成自定義組件,并且自定義組件使用了 PureComponent 或 React.memo 優(yōu)化。那么使用 key 屬性就不只節(jié)省了 DOM 更新,還避免了組件的 Render 過程。

    React 官方推薦[14]將每項數(shù)據(jù)的 ID 作為組件的 key,以達到上述的優(yōu)化目的。并且不推薦使用_每項的索引_作為 key,因為傳索引作為 key 時,就會退化為不使用 key 時的代碼。那么是否在所有列表渲染的場景下,使用 ID 都優(yōu)于使用索引呢?

    答案是否定的,在常見的分頁列表中,第一頁和第二頁的列表項 ID 都是不同,假設每頁展示三條數(shù)據(jù),那么切換頁面前后組件 Render 結果如下。

     
     
     
     
    1.   
    2. dataA
    3.   
    4. dataB
    5.   
    6. dataC
    7.   
    8.   
    9. dataD
    10.   
    11. dataE
    12.   
    13. dataF
    14.   
    15. 復制代碼 

    切換到第二頁后,由于所有

  • 的 key 值不同,所以 Diff 算法會將第一頁的所有 DOM 節(jié)點標記為刪除,然后將第二頁的所有 DOM 節(jié)點標記為新增。整個更新過程需要三次 DOM 刪除、三次 DOM 創(chuàng)建。如果不使用 key,Diff 算法只會將三個
  • 節(jié)點標記為更新,執(zhí)行三次 DOM 更新。參考 Demo 沒有添加、刪除、排序功能的分頁列表[15],使用 key 時每次翻頁耗時約為 140ms,而不使用 key 僅為 70ms。

    盡管存在以上場景,React 官方仍然推薦使用 ID 作為每項的 key 值。其原因有兩:

        1.  在列表中執(zhí)行刪除、插入、排序列表項的操作時,使用 ID 作為 key 將更高效。而翻頁操作往往伴隨著 API 請求,DOM 操作耗時遠小于 API 請求耗時,是否使用 ID 在該場景下對用戶體驗影響不大。

        2.  使用 ID 做為 key 可以維護該 ID 對應的列表項組件的 State。舉個例子,某表格中每列都有普通態(tài)和編輯態(tài)兩個狀態(tài),起初所有列都是普通態(tài),用戶點擊第一行第一列,使其進入編輯態(tài)。然后用戶又拖拽第二行,將其移動到表格的第一行。如果開發(fā)者使用索引作為 key,那么第一行第一列的狀態(tài)仍然為編輯態(tài),而用戶實際希望編輯的是第二行的數(shù)據(jù),在用戶看來就是不符合預期的。盡管這個問題可以通過將「是否處于編輯態(tài)」存放在數(shù)據(jù)項的數(shù)據(jù)中,利用 Props 來解決,但是使用 ID 作為 key 不是更香嗎?

    7. useMemo 返回虛擬 DOM

    利用 useMemo 可以緩存計算結果的特點,如果 useMemo 返回的是組件的虛擬 DOM,則將在 useMemo 依賴不變時,跳過組件的 Render 階段。該方式與 React.memo 類似,但與 React.memo 相比有以下優(yōu)勢:

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

    例子參考:useMemo 跳過組件 Render 過程[17]。該例子中,父組件狀態(tài)更新后,不使用 useMemo 的子組件會執(zhí)行 Render 過程,而使用 useMemo 的子組件不會執(zhí)行。

     
     
     
     
    1. import { useEffect, useMemo, useState } from "react"  
    2. import "./styles.css"  
    3. const renderCntMap = {}  
    4. function Comp({ name }) {  
    5.   renderCntMap[name] = (renderCntMap[name] || 0) + 1  
    6.   return (  
    7.     
        
    8.       組件「{name}」 Render 次數(shù):{renderCntMap[name]}  
    9.     
  •   
  •   )  
  • }   
  • export default function App() {  
  •   const setCnt = useState(0)[1]  
  •   useEffect(() => {  
  •     const timer = window.setInterval(() => {  
  •       setCnt(v => v + 1)  
  •     }, 1000)  
  •     return () => clearInterval(timer)  
  •   }, [setCnt])   
  •   const comp = useMemo(() => {  
  •     return   
  •   }, [])  
  •   return (  
  •       
  •         
  •       {comp}  
  •     
  •   
  •   ) 
  • }  
  • 復制代碼 
  • 8. 跳過回調函數(shù)改變觸發(fā)的 Render 過程

    React 組件的 Props 可以分為兩類。a) 一類是在對組件 Render 有影響的屬性,如:頁面數(shù)據(jù)、getPopupContainer[18] 和 renderProps 函數(shù)。b) 另一類是組件 Render 后的回調函數(shù),如:onClick、onVisibleChange[19]。b) 類屬性并不參與到組件的 Render 過程,因為可以對 b) 類屬性進行優(yōu)化。當 b)類屬性發(fā)生改變時,不觸發(fā)組件的重新 Render ,而是在回調觸發(fā)時調用最新的回調函數(shù)。

    Dan Abramov 在 A Complete Guide to useEffect[20] 文章中認為,每次 Render 都有自己的事件回調是一件很酷的特性。但該特性要求每次回調函數(shù)改變就觸發(fā)組件的重新 Render ,這在性能優(yōu)化過程中是可以取舍的。

    例子參考:跳過回調函數(shù)改變觸發(fā)的 Render 過程[21]。Demo 中通過攔截子組件的 Props 實現(xiàn),僅僅是因為筆者比較懶不想改了,這種實現(xiàn)方式也能開闊讀者視野吧。實際上該優(yōu)化思想應該通過 useMemo/React.memo 實現(xiàn),且使用 useMemo 實現(xiàn)時也更容易理解。

     
     
     
     
    1. import { Children, cloneElement, memo, useEffect, useRef } from "react"  
    2. import { useDeepCompareMemo } from "use-deep-compare"  
    3. import omit from "lodash.omit"  
    4. let renderCnt = 0  
    5. export function SkipNotRenderProps({ children, skips }) {  
    6.   if (!skips) {  
    7.     // 默認跳過所有回調函數(shù)  
    8.     skips = prop => prop.startsWith("on")  
    9.   }  
    10.   const child = Children.only(children)  
    11.   const childchildProps = child.props  
    12.   const propsRef = useRef({})  
    13.   const nextSkippedPropsRef = useRef({})  
    14.   Object.keys(childProps)  
    15.     .filter(it => skips(it))  
    16.     .forEach(key => {  
    17.       // 代理函數(shù)只會生成一次,其值始終不變  
    18.       nextSkippedPropsRef.current[key] =  
    19.         nextSkippedPropsRef.current[key] ||  
    20.         function skipNonRenderPropsProxy(...args) {  
    21.           propsRef.current[key].apply(this, args)  
    22.         }  
    23.     })  
    24.   useEffect(() => {  
    25.     propsRef.current = childProps  
    26.   })  
    27.   // 這里使用 useMemo 優(yōu)化技巧  
    28.   // 除去回調函數(shù),其他屬性改變生成新的 React.Element  
    29.   return useShallowCompareMemo(() => {  
    30.     return cloneElement(child, {  
    31.       ...child.props,  
    32.       ...nextSkippedPropsRef.current,  
    33.     })  
    34.   }, [omit(childProps, Object.keys(nextSkippedPropsRef.current))])  
    35. }  
    36. // SkipNotRenderPropsComp 組件內容和 Normal 內容一樣  
    37. export function SkipNotRenderPropsComp({ onClick }) {  
    38.   return (  
    39.       
    40.         
    41.         跳過『與 Render 無關的 Props』改變觸發(fā)的重新 Render  
    42.       
      
  •       Render 次數(shù)為:{++renderCnt}  
  •       
      
  •           
  •           點我回調,回調彈出值為 1000(優(yōu)化成功)  
  •           
  •       
  •   
  •     
  •   
  •   )  
  • }  
  • export default SkipNotRenderPropsComp  
  • 復制代碼 
  • 9. Hooks 按需更新

    如果自定義 Hook 暴露多個狀態(tài),而調用方只關心某一個狀態(tài),那么其他狀態(tài)改變就不應該觸發(fā)組件重新 Render。

     
     
     
     
    1. export const useNormalDataHook = () => {  
    2.   const [data, setData] = useState({ info: null, count: null })  
    3.   useEffect(() => {  
    4.     const timer = setInterval(() => {  
    5.       setData(data => ({  
    6.         ...data,  
    7.         count: data.count + 1,  
    8.       }))  
    9.     }, 1000)  
    10.     return () => {  
    11.       clearInterval(timer)  
    12.     }  
    13.   })  
    14.   return data  
    15. }  
    16. 復制代碼 

    如上所示,useNormalDataHook 暴露了兩個狀態(tài) info 和 count 給調用方,如果調用方只關心 info 字段,那么 count 改變就沒必要觸發(fā)調用方組件 Render。

    按需更新主要通過兩步來實現(xiàn),參考Hooks 按需更新[22]

    1.  根據(jù)調用方使用的數(shù)據(jù)進行依賴收集,Demo 中使用 Object.defineProperties 實現(xiàn)。
    2.  只在依賴發(fā)生改變時才觸發(fā)組件更新。

    10. 動畫庫直接修改 DOM 屬性

    這個優(yōu)化在業(yè)務中應該用不上,但還是非常值得學習的,將來可以應用到組件庫中。參考 react-spring[23] 的動畫實現(xiàn),當一個動畫啟動后,每次動畫屬性改變不會引起組件重新 Render ,而是直接修改了 dom 上相關屬性值。

    例子演示:CodeSandbox 在線 Demo[24]

     
     
     
     
    1. import React, { useState } from "react"  
    2. import { useSpring, animated as a } from "react-spring"  
    3. import "./styles.css"  
    4. let renderCnt = 0  
    5. export function Card() {  
    6.   const [flipped, set] = useState(false)  
    7.   const { transform, opacity } = useSpring({  
    8.     opacity: flipped ? 1 : 0,  
    9.     transform: `perspective(600px) rotateX(${flipped ? 180 : 0}deg)`,  
    10.     config: { mass: 5, tension: 500, friction: 80 },  
    11.   })  
    12.   // 盡管 opacity 和 transform 的值在動畫期間一直變化  
    13.   // 但是并沒有組件的重新 Render  
    14.   return (  
    15.      set(state => !state)}>  
    16.         
    17.         Render 次數(shù):{++renderCnt}  
    18.         
    19.       
    20.         class="c back"  
    21.         style={{ opacity: opacity.interpolate(o => 1 - o), transform }}  
    22.       />  
    23.       
    24.         class="c front"  
    25.         style={{  
    26.           opacity,  
    27.           transform: transform.interpolate(t => `${t} rotateX(180deg)`),  
    28.         }}  
    29.       />  
    30.       
    31.   )  
    32. }  
    33. export default Card  
    34. 復制代碼 

    提交階段優(yōu)化

    這類優(yōu)化的目的是減少提交階段耗時,該分類中僅有一條優(yōu)化技巧。

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

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

    React 工作流[25]提交階段的第二步就是執(zhí)行提交階段鉤子,它們的執(zhí)行會阻塞瀏覽器更新頁面。如果在提交階段鉤子函數(shù)中更新組件 State,會再次觸發(fā)組件的更新流程,造成兩倍耗時。

    一般在提交階段的鉤子中更新組件狀態(tài)的場景有:

        1.  計算并更新組件的派生狀態(tài)(Derived State)。在該場景中,類組件應使用 getDerivedStateFromProps[26] 鉤子方法代替,函數(shù)組件應使用函數(shù)調用時執(zhí)行 setState[27]的方式代替。使用上面兩種方式后,React 會將新狀態(tài)和派生狀態(tài)在一次更新內完成。

        2.  根據(jù) DOM 信息,修改組件狀態(tài)。在該場景中,除非想辦法不依賴 DOM 信息,否則兩次更新過程是少不了的,就只能用其他優(yōu)化技巧了。

    use-swr 的源碼[28]就使用了該優(yōu)化技巧。當某個接口存在緩存數(shù)據(jù)時,use-swr 會先使用該接口的緩存數(shù)據(jù),并在 requestIdleCallback 時再重新發(fā)起請求,獲取最新數(shù)據(jù)。如果 use-swr 不做該優(yōu)化的話,就會在 useLayoutEffect 中觸發(fā)重新驗證并設置 isValidating 狀態(tài)為 true[29],引起組件的更新流程,造成性能損失。

    前端通用優(yōu)化

    這類優(yōu)化在所有前端框架中都存在,本文的重點就在于將這些技巧應用在 React 組件中。

    1. 組件按需掛載

    組件按需掛載優(yōu)化又可以分為懶加載、懶渲染和虛擬列表三類。

    懶加載

    在 SPA 中,懶加載優(yōu)化一般用于從一個路由跳轉到另一個路由。還可用于用戶操作后才展示的復雜組件,比如點擊按鈕后展示的彈窗模塊(有時候彈窗就是一個復雜頁面 )。在這些場景下,結合 Code Split 收益較高。

    懶加載的實現(xiàn)是通過 Webpack 的動態(tài)導入和 React.lazy 方法,

    參考例子 lazy-loading[30]。實現(xiàn)懶加載優(yōu)化時,不僅要考慮加載態(tài),還需要對加載失敗進行容錯處理。

     
     
     
     
    1. import { lazy, Suspense, Component } from "react"  
    2. import "./styles.css"  
    3. // 對加載失敗進行容錯處理  
    4. class ErrorBoundary extends Component {  
    5.   constructor(props) {  
    6.     super(props)  
    7.     this.state = { hasError: false }  
    8.   }  
    9.   static getDerivedStateFromError(error) {  
    10.     return { hasError: true }  
    11.   }  
    12.   render() {  
    13.     if (this.state.hasError
      新聞名稱:React 性能優(yōu)化 :包括原理、技巧、Demo、工具使用
      網頁地址:http://www.5511xx.com/article/djhgsee.html
    14. <td id="qn6c1"><code id="qn6c1"></code></td><td id="qn6c1"><code id="qn6c1"><em id="qn6c1"></em></code></td>