新聞中心
本文分為三部分,首先介紹 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]。
拓展知識
- 建議對 React 生命周期不熟悉的讀者結(jié)合 React 組件的生命周期圖[3]閱讀本文。記得勾選該網(wǎng)站上的復(fù)選框。
- 因?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)化技巧分為三大類,分別為:
- 跳過不必要的組件更新。這類優(yōu)化是在組件狀態(tài)發(fā)生變更后,通過減少不必要的組件更新來實(shí)現(xiàn),是本文優(yōu)化技巧的主要部分。
- 提交階段優(yōu)化。這類優(yōu)化的目的是減少提交階段耗時(shí),該分類中僅有一條優(yōu)化技巧。
- 前端通用優(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è)弊端。
- 如果存在很多子孫組件,「找出所有子孫組件使用的屬性」就會有很多工作量,也容易因?yàn)槁y導(dǎo)致 bug。
- 存在潛在的工程隱患。舉例來說,假設(shè)組件結(jié)構(gòu)如下。
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)。
- import { useState, useEffect, createContext, useContext } from "react"
- const renderCntMap = {}
- const renderOnce = name => {
- return (renderCntMap[name] = (renderCntMap[name] || 0) + 1)
- }
- // 將需要公共訪問的部分移動(dòng)到 Context 中進(jìn)行優(yōu)化
- // Context.Provider 就是發(fā)布者
- // Context.Consumer 就是消費(fèi)者
- const ValueCtx = createContext()
- const CtxContainer = ({ children }) => {
- const [cnt, setCnt] = useState(0)
- useEffect(() => {
- const timer = window.setInterval(() => {
- setCnt(v => v + 1)
- }, 1000)
- return () => clearInterval(timer)
- }, [setCnt])
- return
{children} - }
- function CompA({}) {
- const cnt = useContext(ValueCtx)
- // 組件內(nèi)使用 cnt
- return
組件 CompA Render 次數(shù):{renderOnce("CompA")}- }
- function CompB({}) {
- const cnt = useContext(ValueCtx)
- // 組件內(nèi)使用 cnt
- return
組件 CompB Render 次數(shù):{renderOnce("CompB")}- }
- function CompC({}) {
- return
組件 CompC Render 次數(shù):{renderOnce("CompC")}- }
- export const PubSubCommunicate = () => {
- return (
優(yōu)化后場景
- 將狀態(tài)提升至最低公共祖先的上層,用 CtxContainer 將其內(nèi)容包裹。
- 每次 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 改變會引起
- import { useState } from "react"
- export default function App() {
- let [color, setColor] = useState("red")
- return (
- setColor(e.target.value)} />
Hello, world!
- )
- }
- 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é)果如下。
- export default function App() {
- return (
- <>
- >
- )
- }
- function Form() {
- let [color, setColor] = useState("red")
- return (
- <>
- setColor(e.target.value)} />
Hello, world!
- >
- )
- }
- 復(fù)制代碼
這樣調(diào)整之后,color 改變就不會引起組件 App 和 ExpensiveTree 重新 Render 了。
如果對上面的場景進(jìn)行擴(kuò)展,在組件 App 的頂層和子樹中都使用了狀態(tài) color ,但
- import { useState } from "react"
- export default function App() {
- let [color, setColor] = useState("red")
- return (
- setColor(e.target.value)} />
Hello, world!
- )
- }
- 復(fù)制代碼
在這種場景中,我們?nèi)匀粚?color 狀態(tài)抽取到新組件中,并提供一個(gè)插槽來組合
- import { useState } from "react"
- export default function App() {
- return
}> - }
- function ColorContainer({ expensiveTreeNode }) {
- let [color, setColor] = useState("red")
- return (
- setColor(e.target.value)} />
- {expensiveTreeNode}
Hello, world!
- )
- }
- 復(fù)制代碼
這樣調(diào)整之后,color 改變就不會引起組件 App 和 ExpensiveTree 重新 Render 了。
該優(yōu)化技巧來源于 before-you-memo[13],Dan 認(rèn)為這種優(yōu)化方式在 Server Component 場景下更有效,因?yàn)?
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é)果如下。
- Duke
- Villanova
- Connecticut
- Duke
- Villanova
- 復(fù)制代碼
此時(shí) React 的 Diff 算法會按照
如果加上 React 的 key 屬性,兩次 Render 結(jié)果如下。
- Duke
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 更新操作。
如果把例子中的
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é)果如下。
- dataA
切換到第二頁后,由于所有
盡管存在以上場景,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)勢:
- 更方便。React.memo 需要對組件進(jìn)行一次包裝,生成新的組件。而 useMemo 只需在存在性能瓶頸的地方使用,不用修改組件。
- 更靈活。useMemo 不用考慮組件的所有 Props,而只需考慮當(dāng)前場景中用到的值,也可使用 useDeepCompareMemo[16] 對用到的值進(jìn)行深比較。
例子參考:useMemo 跳過組件 Render 過程[17]。該例子中,父組件狀態(tài)更新后,不使用 useMemo 的子組件會執(zhí)行 Render 過程,而使用 useMemo 的子組件不會執(zhí)行。
- import { useEffect, useMemo, useState } from "react"
- import "./styles.css"
- const renderCntMap = {}
- function Comp({ name }) {
- renderCntMap[name] = (renderCntMap[name] || 0) + 1
- return (
- 組件「{name}」 Render 次數(shù):{renderCntMap[name]}
- )
- }
- 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í)也更容易理解。
- import { Children, cloneElement, memo, useEffect, useRef } from "react"
- import { useDeepCompareMemo } from "use-deep-compare"
- import omit from "lodash.omit"
- let renderCnt = 0
- export function SkipNotRenderProps({ children, skips }) {
- if (!skips) {
- // 默認(rèn)跳過所有回調(diào)函數(shù)
- skips = prop => prop.startsWith("on")
- }
- const child = Children.only(children)
- const childchildProps = child.props
- const propsRef = useRef({})
- const nextSkippedPropsRef = useRef({})
- Object.keys(childProps)
- .filter(it => skips(it))
- .forEach(key => {
- // 代理函數(shù)只會生成一次,其值始終不變
- nextSkippedPropsRef.current[key] =
- nextSkippedPropsRef.current[key] ||
- function skipNonRenderPropsProxy(...args) {
- propsRef.current[key].apply(this, args)
- }
- })
- useEffect(() => {
- propsRef.current = childProps
- })
- // 這里使用 useMemo 優(yōu)化技巧
- // 除去回調(diào)函數(shù),其他屬性改變生成新的 React.Element
- return useShallowCompareMemo(() => {
- return cloneElement(child, {
- ...child.props,
- ...nextSkippedPropsRef.current,
- })
- }, [omit(childProps, Object.keys(nextSkippedPropsRef.current))])
- }
- // SkipNotRenderPropsComp 組件內(nèi)容和 Normal 內(nèi)容一樣
- export function SkipNotRenderPropsComp({ onClick }) {
- return (
- 跳過『與 Render 無關(guān)的 Props』改變觸發(fā)的重新 Render
- 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。
- export const useNormalDataHook = () => {
- const [data, setData] = useState({ info: null, count: null })
- useEffect(() => {
- const timer = setInterval(() => {
- setData(data => ({
- ...data,
- count: data.count + 1,
- }))
- }, 1000)
- return () => {
- clearInterval(timer)
- }
- })
- return data
- }
- 復(fù)制代碼
如上所示,useNormalDataHook 暴露了兩個(gè)狀態(tài) info 和 count 給調(diào)用方,如果調(diào)用方只關(guān)心 info 字段,那么 count 改變就沒必要觸發(fā)調(diào)用方組件 Render。
按需更新主要通過兩步來實(shí)現(xiàn),參考Hooks 按需更新[22]
- 根據(jù)調(diào)用方使用的數(shù)據(jù)進(jìn)行依賴收集,Demo 中使用 Object.defineProperties 實(shí)現(xiàn)。
- 只在依賴發(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]
- import React, { useState } from "react"
- import { useSpring, animated as a } from "react-spring"
- import "./styles.css"
- let renderCnt = 0
- export function Card() {
- const [flipped, set] = useState(false)
- const { transform, opacity } = useSpring({
- opacity: flipped ? 1 : 0,
- transform: `perspective(600px) rotateX(${flipped ? 180 : 0}deg)`,
- config: { mass: 5, tension: 500, friction: 80 },
- })
- // 盡管 opacity 和 transform 的值在動(dòng)畫期間一直變化
- // 但是并沒有組件的重新 Render
- return (
set(state => !state)}>- Render 次數(shù):{++renderCnt}
- class="c back"
- style={{ opacity: opacity.interpolate(o => 1 - o), transform }}
- />
- class="c front"
- style={{
- opacity,
- transform: transform.interpolate(t => `${t} rotateX(180deg)`),
- }}
- />
- )
- }
- export default Card
- 復(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ò)處理。
- import { lazy, Suspense, Component } from "react"
- import "./styles.css"
- // 對加載失敗進(jìn)行容錯(cuò)處理
- class ErrorBoundary extends Component {
- constructor(props) {
- super(props)
- this.state = { hasError: false }
- }
- static getDerivedStateFromError(error) {
- return { hasError: true }
- }
- render() {
- if (this.state.hasError
網(wǎng)頁標(biāo)題:React 性能優(yōu)化 :包括原理、技巧、Demo、工具使用
標(biāo)題來源:http://fisionsoft.com.cn/article/djhgsee.html


咨詢
建站咨詢
