日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

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

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

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

React 工作流

React 是聲明式 UI 庫,負(fù)責(zé)將 State 轉(zhuǎn)換為頁面結(jié)構(gòu)(虛擬 DOM 結(jié)構(gòu))后,再轉(zhuǎn)換成真實(shí) DOM 結(jié)構(gòu),交給瀏覽器渲染。當(dāng) State 發(fā)生改變時(shí),React 會先進(jìn)行調(diào)和(Reconciliation)階段,調(diào)和階段結(jié)束后立刻進(jìn)入提交(Commit)階段,提交階段結(jié)束后,新 State 對應(yīng)的頁面才被展示出來。

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

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

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

拓展知識

  1.   建議對 React 生命周期不熟悉的讀者結(jié)合 React 組件的生命周期圖[3]閱讀本文。記得勾選該網(wǎng)站上的復(fù)選框。
  2.   因?yàn)槔斫馐录h(huán)后才知道頁面會在什么時(shí)候被更新,所以推薦一個(gè)介紹事件循環(huán)的視頻[4]。該視頻中事件循環(huán)的偽代碼如下圖,非常清晰易懂。

定義 Render 過程

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

優(yōu)化技巧

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

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

跳過不必要的組件更新

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

1. PureComponent、React.memo

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

2. shouldComponentUpdate

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

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

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

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

  1.  如果存在很多子孫組件,「找出所有子孫組件使用的屬性」就會有很多工作量,也容易因?yàn)槁y導(dǎo)致 bug。
  2.  存在潛在的工程隱患。舉例來說,假設(shè)組件結(jié)構(gòu)如下。 
 
 
 
 
  1.   
  2.   {/* B 組件只使用了 data.a 和 data.b */}  
  3.     
  4.     {/* C 組件只使用了 data.a */}  
  5.       
  6.     
  7.   
  8. 復(fù)制代碼 

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

拓展知識

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

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

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

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

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

拓展知識

useCallback 是「useMemo 的返回值為函數(shù)」時(shí)的特殊情況,是 React 提供的便捷方式。在 React Server Hooks 代碼[9] 中,useCallback 就是基于 useMemo 實(shí)現(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 過程,但中間組件并不關(guān)心該狀態(tài),它的 Render 過程只負(fù)責(zé)將該狀態(tài)再傳給子組件。在這種場景下可以將狀態(tài)用發(fā)布者訂閱者模式維護(hù),只有關(guān)心該狀態(tài)的組件才去訂閱該狀態(tài),不再需要中間組件傳遞該狀態(tài)。當(dāng)狀態(tài)更新時(shí),發(fā)布者發(fā)布數(shù)據(jù)更新消息,只有訂閱者組件才會觸發(fā) Render 過程,中間組件不再執(zhí)行 Render 過程。

只要是發(fā)布者訂閱者模式的庫,都可以進(jìn)行該優(yōu)化。比如:redux、use-global-state、React.createContext 等。例子參考:發(fā)布者訂閱者模式跳過中間組件的渲染階段[12],本示例使用 React.createContext 進(jìn)行實(shí)現(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. // 將需要公共訪問的部分移動(dòng)到 Context 中進(jìn)行優(yōu)化  
  7. // Context.Provider 就是發(fā)布者  
  8. // Context.Consumer 就是消費(fèi)者  
  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.   // 組件內(nèi)使用 cnt  
  23.   return 
    組件 CompA Render 次數(shù):{renderOnce("CompA")}
      
  24. }  
  25. function CompB({}) {  
  26.   const cnt = useContext(ValueCtx)  
  27.   // 組件內(nèi)使用 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 將其內(nèi)容包裹。  
  40.         
  
  •           
  •           每次 Render 時(shí),只有組件A和組件B會重新 Render 。  
  •         
  •   
  •           
  •           父組件 Render 次數(shù):{renderOnce("parent")}  
  •         
  •   
  •           
  •           
  •           
  •       
  •   
  •       
  •   )  
  • }   
  • export default PubSubCommunicate  
  • 復(fù)制代碼 
  • 5. 狀態(tài)下放,縮小狀態(tài)影響范圍

    如果一個(gè)狀態(tài)只在某部分子樹中使用,那么可以將這部分子樹提取為組件,并將該狀態(tài)移動(dòng)到該組件內(nè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.

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

    提取到組件 Form 中,結(jié)果如下。

     
     
     
     
    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. 復(fù)制代碼 

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

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

     
     
     
     
    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.     
      
  •   )  
  • }  
  • 復(fù)制代碼 
  • 在這種場景中,我們?nèi)匀粚?color 狀態(tài)抽取到新組件中,并提供一個(gè)插槽來組合 ,如下所示。

     
     
     
     
    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.     
      
  •   )  
  • }  
  • 復(fù)制代碼 
  • 這樣調(diào)整之后,color 改變就不會引起組件 App 和 ExpensiveTree 重新 Render 了。

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

    6. 列表項(xiàng)使用 key 屬性

    當(dāng)渲染列表項(xiàng)時(shí),如果不給組件設(shè)置不相等的屬性 key,就會收到如下報(bào)警。

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

     
     
     
     
    1.   
      •   
      •   
      • Duke
      •   
      •   
      • Villanova
      •   
        
    2.   
      •   
      •   
      • Connecticut
      •   
      •   
      • Duke
      •   
      •   
      • Villanova
      •   
        
    3. 復(fù)制代碼 

    此時(shí) React 的 Diff 算法會按照

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

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

     
     
     
     
    1.   
      •   
      •   Duke
      •   
      •   Villanova
      •   
        
    2.   
      •   
      •   Connecticut
      •   
      •   Duke
      •   
      •   Villanova
      •  
        
    3. 復(fù)制代碼 

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

    如果把例子中的

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

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

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

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

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

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

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

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

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

    7. useMemo 返回虛擬 DOM

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

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

    例子參考: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}  
  •     
  •   
  •   ) 
  • }  
  • 復(fù)制代碼 
  • 8. 跳過回調(diào)函數(shù)改變觸發(fā)的 Render 過程

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

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

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

     
     
     
     
    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.     // 默認(rèn)跳過所有回調(diào)函數(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.   // 除去回調(diào)函數(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 組件內(nèi)容和 Normal 內(nèi)容一樣  
    37. export function SkipNotRenderPropsComp({ onClick }) {  
    38.   return (  
    39.       
    40.         
    41.         跳過『與 Render 無關(guān)的 Props』改變觸發(fā)的重新 Render  
    42.       
      
  •       Render 次數(shù)為:{++renderCnt}  
  •       
      
  •           
  •           點(diǎn)我回調(diào),回調(diào)彈出值為 1000(優(yōu)化成功)  
  •           
  •       
  •   
  •     
  •   
  •   )  
  • }  
  • export default SkipNotRenderPropsComp  
  • 復(fù)制代碼 
  • 9. Hooks 按需更新

    如果自定義 Hook 暴露多個(gè)狀態(tài),而調(diào)用方只關(guān)心某一個(gè)狀態(tài),那么其他狀態(tài)改變就不應(yīng)該觸發(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. 復(fù)制代碼 

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

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

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

    10. 動(dòng)畫庫直接修改 DOM 屬性

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

    例子演示: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 的值在動(dòng)畫期間一直變化  
    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. 復(fù)制代碼 

    提交階段優(yōu)化

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

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

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

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

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

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

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

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

    前端通用優(yōu)化

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

    1. 組件按需掛載

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

    懶加載

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

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

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

     
     
     
     
    1. import { lazy, Suspense, Component } from "react"  
    2. import "./styles.css"  
    3. // 對加載失敗進(jìn)行容錯(cuò)處理  
    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
      標(biāo)題名稱:React 性能優(yōu)化 :包括原理、技巧、Demo、工具使用
      轉(zhuǎn)載來于:http://www.dlmjj.cn/article/djhgsee.html
      <small id="fahgq"><button id="fahgq"><rt id="fahgq"></rt></button></small>
    14. <small id="fahgq"><button id="fahgq"><rt id="fahgq"></rt></button></small>