React?性能優(yōu)化方法總結(jié)
前言
要講清楚性能優(yōu)化的原理,就需要知道它的前世今生,需要回答如下的問題:
- React 是如何進行頁面渲染的?
- 造成頁面的卡頓的罪魁禍首是什么呢?
- 我們?yōu)槭裁葱枰阅軆?yōu)化?
- React 有哪些場景會需要性能優(yōu)化?
- React 本身的性能優(yōu)化手段?
- 還有哪些工具可以提升性能呢?
為什么頁面會出現(xiàn)卡頓的現(xiàn)象?
為什么瀏覽器會出現(xiàn)頁面卡頓的問題?是不是瀏覽器不夠先進?這都 2202 年了,怎么還會有這種問題呢?
實際上問題的根源來源于瀏覽器的刷新機制。
我們?nèi)祟愌劬Φ乃⑿侣适?60Hz,瀏覽器依據(jù)人眼的刷新率 計算出了
1000 Ms / 60 = 16.6ms
也就是說,瀏覽器要在16.6Ms 進行一次刷新,人眼就不會感覺到卡頓,而如果超過這個時間進行刷新,就會感覺到卡頓。
而瀏覽器的主進程在僅僅需要頁面的渲染,還需要做解析執(zhí)行Js,他們運行在一個進程中。
如果js的在執(zhí)行的長時間占用主進程的資源,就會導致沒有資源進行頁面的渲染刷新,進而導致頁面的卡頓。
那么這個又和 React 的性能優(yōu)化又有什么關(guān)系呢?
React 到底是在哪里出現(xiàn)了卡頓?
基于我們上的知識,js 長期霸占瀏覽器主線程造成無法刷新而造成卡頓。
那么 React 的卡頓也是基于這個原因。
React 在render的時候,會根據(jù)現(xiàn)有render產(chǎn)生的新的jsx的數(shù)據(jù)和現(xiàn)有fiberRoot 進行比對,找到不同的地方,然后生成新的workInProgress,進而在掛載階段把新的workInProgress交給服務(wù)器渲染。
在這個過程中,React 為了讓底層機制更高效快速,進行了大量的優(yōu)化處理,如設(shè)立任務(wù)優(yōu)先級、異步調(diào)度、diff算法、時間分片等。
整個鏈路就是了高效快速的完成從數(shù)據(jù)更新到頁面渲染的整體流程。
為了不讓遞歸遍歷尋找所有更新節(jié)點太大而占用瀏覽器資源,React 升級了fiber架構(gòu),時間分片,讓其可以增量更新。
為了找出所有的更新節(jié)點,設(shè)立了diff算法,高效的查找所有的節(jié)點。
為了更高效的更新,及時響應(yīng)用戶的操作,設(shè)計任務(wù)調(diào)度優(yōu)先級。
而我們的性能優(yōu)化就是為了不給 React 拖后腿,讓其更快,更高效的遍歷。
那么性能優(yōu)化的奧義是什么呢??
就是控制刷新渲染的波及范圍,我們只讓改更新的更新,不該更新的不要更新,讓我們的更新鏈路盡可能的短的走完,那么頁面當然就會及時刷新不會卡頓了。
React 有哪些場景會需要性能優(yōu)化?
- 父組件刷新,而不波及子組件
- 組件自己控制自己是否刷新
- 減少波及范圍,無關(guān)刷新數(shù)據(jù)不存入state中
- 合并 state,減少重復(fù) setState 的操作
- 如何更快的完成diff的比較,加快進程
我們分別從這些場景說一下:
一:父組件刷新,而不波及子組件。
我們知道 React 在組件刷新判定的時候,如果觸發(fā)刷新,那么它會深度遍歷所有子組件,查找所有更新的節(jié)點,依據(jù)新的jsx數(shù)據(jù)和舊的 fiber ,生成新的workInProgress,進而進行頁面渲染。
所以父組件刷新的話,子組件必然會跟著刷新,但是假如這次的刷新,和我們子組件沒有關(guān)系呢?怎么減少這種波及呢?
如下面這樣:
export default function Father1 (){ let [name,setName] = React.useState(''); return ( <div> <button onClick={()=>setName("獲取到的數(shù)據(jù)")}>點擊獲取數(shù)據(jù)</button> {name} <Children/> </div> ) } function Children(){ return ( <div> 這里是子組件 </div> ) }
運行結(jié)果:
可以看到我們的子組件被波及了,解決辦法有很多,總體來說分為兩種:
- 子組件自己判斷是否需要更新 ,典型的就是 PureComponent,shouldComponentUpdate,memo
- 父組件對子組件做個緩沖判斷
第一種:使用 PureComponent
使用 PureComponent 的原理就是它會對state 和props進行淺比較,如果發(fā)現(xiàn)并不相同就會更新。
export default function Father1 (){ let [name,setName] = React.useState(''); return ( <div> <button onClick={()=>setName("父組件的數(shù)據(jù)")}>點擊刷新父組件</button> {name} <Children1/> </div> ) } class Children extends React.PureComponent{ render() { return ( <div>這里是子組件</div> ) } }
執(zhí)行結(jié)果:
實際上PureComponent
就是在內(nèi)部更新的時候調(diào)用了會調(diào)用如下方法來判斷 新舊state和props
function shallowEqual(objA: mixed, objB: mixed): boolean { if (is(objA, objB)) { return true; } if ( typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. for (let i = 0; i < keysA.length; i++) { const currentKey = keysA[i]; if ( !hasOwnProperty.call(objB, currentKey) || !is(objA[currentKey], objB[currentKey]) ) { return false; } } return true; }
它的判斷步驟如下:
- 第一步,首先會直接比較新老
props
或者新老state
是否相等。如果相等那么不更新組件。 - 第二步,判斷新老
state
或者props
,有不是對象或者為null
的,那么直接返回 false ,更新組件。 - 第三步,通過
Object.keys
將新老props
或者新老state
的屬性名key
變成數(shù)組,判斷數(shù)組的長度是否相等,如果不相等,證明有屬性增加或者減少,那么更新組件。 - 第四步,遍歷老
props
或者老state
,判斷對應(yīng)的新props
或新state
,有沒有與之對應(yīng)并且相等的(這個相等是淺比較),如果有一個不對應(yīng)或者不相等,那么直接返回false
,更新組件。 到此為止,淺比較流程結(jié)束,PureComponent
就是這么做渲染節(jié)流優(yōu)化的。
在使用PureComponent時需要注意的細節(jié);
由于PureComponent
使用的是淺比較判斷state
和props
,所以如果我們在父子組件中,子組件使用PureComponent
,在父組件刷新的過程中不小心把傳給子組件的回調(diào)函數(shù)變了,就會造成子組件的誤觸發(fā),這個時候PureComponent
就失效了。
細節(jié)一:函數(shù)組件中,匿名函數(shù),箭頭函數(shù)和普通函數(shù)都會重新聲明
下面這些情況都會造成函數(shù)的重新聲明:
箭頭函數(shù)
<Children1 callback={(value)=>setValue(value)}/>
匿名函數(shù)
<Children1 callback={function (value){setValue(value)}}/>
普通函數(shù)
export default function Father1 (){ let [name,setName] = React.useState(''); let [value,setValue] = React.useState('') const setData=(value)=>{ setValue(value) } return ( <div> <button onClick={()=>setName("父組件的數(shù)據(jù)"+Math.random())}>點擊刷新父組件</button> {name} <Children1 callback={setData}/> </div> ) } class Children1 extends React.PureComponent{ render() { return ( <div>這里是子組件</div> ) } }
執(zhí)行結(jié)果:
可以看到子組件的 PureComponent 完全失效了。這個時候就可以使用useMemo或者 useCallback 出馬了,利用他們緩沖一份函數(shù),保證不會出現(xiàn)重復(fù)聲明就可以了。
export default function Father1 (){ let [name,setName] = React.useState(''); let [value,setValue] = React.useState('') const setData= React.useCallback((value)=>{ setValue(value) },[]) return ( <div> <button onClick={()=>setName("父組件的數(shù)據(jù)"+Math.random())}>點擊刷新父組件</button> {name} <Children1 callback={setData}/> </div> ) }
看結(jié)果:
可以看到我們的子組件這次并沒有參與父組件的刷新,在React Profiler
中也提示,Children1
并沒有渲染。
細節(jié)二:class組件中不使用箭頭函數(shù),匿名函數(shù)
原理和函數(shù)組件中的一樣,class 組件中每一次刷新都會重復(fù)調(diào)用render
函數(shù),那么render
函數(shù)中使用的匿名函數(shù),箭頭函數(shù)就會造成重復(fù)刷新的問題。
export default class Father extends React.PureComponent{ constructor(props) { super(props); this.state = { name:"", count:"", } } render() { return ( <div> <button onClick={()=>this.setState({name:"父組件的數(shù)據(jù)"+Math.random()})}>點擊獲取數(shù)據(jù)</button> {this.state.name} <Children1 callback={()=>this.setState({count:11})}/> </div> ) } }
執(zhí)行結(jié)果:
而優(yōu)化這個非常簡單,只需要把函數(shù)換成普通函數(shù)就可以。
export default class Father extends React.PureComponent{ constructor(props) { super(props); this.state = { name:"", count:"", } } setCount=(count)=>{ this.setState({count}) } render() { return ( <div> <button onClick={()=>this.setState({name:"父組件的數(shù)據(jù)"+Math.random()})}>點擊獲取數(shù)據(jù)</button> {this.state.name} <Children1 callback={this.setCount(111)}/> </div> ) } }
執(zhí)行結(jié)果:
細節(jié)三:在 class 組件的render函數(shù)中bind 函數(shù)
這個細節(jié)是我們在class組件中,沒有在constructor
中進行bind
的操作,而是在render
函數(shù)中,那么由于bind
函數(shù)的特性,它的每一次調(diào)用都會返回一個新的函數(shù),所以同樣會造成PureComponent
的失效
export default class Father extends React.PureComponent{ //... setCount(count){ this.setCount({count}) } render() { return ( <div> <button onClick={()=>this.setState({name:"父組件的數(shù)據(jù)"+Math.random()})}>點擊獲取數(shù)據(jù)</button> {this.state.name} <Children1 callback={this.setCount.bind(this,"11111")}/> </div> ) } }
看執(zhí)行結(jié)果:
優(yōu)化的方式也很簡單,把bind
操作放在constructor
中就可以了。
constructor(props) { super(props); this.state = { name:"", count:"", } this.setCount= this.setCount.bind(this); }
執(zhí)行結(jié)果就不在此展示了。
第二種:shouldComponentUpdate
class 組件中 使用 shouldComponentUpdate 是主要的優(yōu)化方式,它不僅僅可以判斷來自父組件的nextprops
,還可以根據(jù)nextState
和最新的nextContext
來決定是否更新。
class Children2 extends React. PureComponent{ shouldComponentUpdate(nextProps, nextState, nextContext) { //判斷只有偶數(shù)的時候,子組件才會更新 if(nextProps !== this.props && nextProps.count % 2 === 0){ return true; }else{ return false; } } render() { return ( <div> 只有父組件傳入的值等于 2的時候才會更新 {this.props.count} </div> ) } }
它的用法也是非常簡單,就是如果需要更新就返回true,不需要更新就返回false.
第三種:函數(shù)組件如何判斷props的變化的更新呢? 使用 React.memo函數(shù)
React.memo
的規(guī)則是如果想要復(fù)用最后一次渲染結(jié)果,就返回true
,不想復(fù)用就返回false
。 所以它和shouldComponentUpdate
的正好相反,false
才會更新,true
就返回緩沖。
const Children3 = React.memo(function ({count}){ return ( <div> 只有父組件傳入的值是偶數(shù)的時候才會更新 {count} </div> ) },(prevProps, nextProps)=>{ if(nextProps.count % 2 === 0){ return false; }else{ return true; } })
如果我們不傳入第二個函數(shù),而是默認讓 React.memo
包裹一下,那么它只會對props
淺比較一下,并不會有比較state
之類的邏輯。
以上三種都是我們?yōu)榱藨?yīng)對父組件更新觸發(fā)子組件,子組件決定是否更新的實現(xiàn)。 下面我們講一下父組件對子組件緩沖實現(xiàn)的情況:
使用 React.useMemo來實現(xiàn)對子組件的緩沖
看下面這段邏輯,我們的子組件只關(guān)心count
數(shù)據(jù),當我們刷新name
數(shù)據(jù)的時候,并不會觸發(fā)刷新 Children1
子組件,實現(xiàn)了我們對組件的緩沖控制。
export default function Father1 (){ let [count,setCount] = React.useState(0); let [name,setName] = React.useState(0); const render = React.useMemo(()=><Children1 count = {count}/>,[count]) return ( <div> <button onClick={()=>setCount(++count)}>點擊刷新count</button> <br/> <button onClick={()=>setName(++name)}>點擊刷新name</button> <br/> {"count"+count} <br/> {"name"+name} <br/> {render} </div> ) } class Children1 extends React.PureComponent{ render() { return ( <div> 子組件只關(guān)系count 數(shù)據(jù) {this.props.count} </div> ) } }
執(zhí)行結(jié)果: 當我們點擊刷新name數(shù)據(jù)時,可以看到?jīng)]有子組件參與刷新
當我們點擊刷新count 數(shù)據(jù)時,子組件參與了刷新
一:組件自己控制自己是否刷新
這里就需要用到上面提到的shouldComponentUpdate
以及PureComponent
,這里不再贅述。
三:減少波及范圍,無關(guān)刷新數(shù)據(jù)不存入state中
這種場景就是我們有意識的控制,如果有一個數(shù)據(jù)我們在頁面上并沒有用到它,但是它又和我們的其他的邏輯有關(guān)系,那么我們就可以把它存儲在其他的地方,而不是state中。
場景一:無意義重復(fù)調(diào)用setState,合并相關(guān)的state
export default class Father extends React.Component{ state = { count:0, name:"", } getData=(count)=>{ this.setState({count}); //依據(jù)異步獲取數(shù)據(jù) setTimeout(()=>{ this.setState({ name:"異步獲取回來的數(shù)據(jù)"+count }) },200) } componentDidUpdate(prevProps, prevState, snapshot) { console.log("渲染次數(shù),",++count,"次") } render() { return ( <div> <button onClick={()=>this.getData(++this.state.count)}>點擊獲取數(shù)據(jù)</button> {this.state.name} </div> ) } }
React Profiler
的執(zhí)行結(jié)果:
可以看到我們的父組件執(zhí)行了兩次。 其中的一次是無意義的先setState
保存一次數(shù)據(jù),然后又根據(jù)這個數(shù)據(jù)異步獲取了數(shù)據(jù)以后又調(diào)用了一次setState
,造成了第二次的數(shù)據(jù)刷新.
而解決辦法就是把這個數(shù)據(jù)合并到異步數(shù)據(jù)獲取完成以后,一起更新到state中。
getData=(count)=>{ //依據(jù)異步獲取數(shù)據(jù) setTimeout(()=>{ this.setState({ name:"異步獲取回來的數(shù)據(jù)"+count, count }) },200) }
看執(zhí)行結(jié)果:只渲染了一次。
場景二:和頁面刷新沒有相關(guān)的數(shù)據(jù),不存入state中
實際上我們發(fā)現(xiàn)這個數(shù)據(jù)在頁面上并沒有展示,我們并不需要把他們都存放在state 中,所以我們可以把這個數(shù)據(jù)存儲在state之外的地方。
export default class Father extends React.Component{ constructor(props) { super(props); this.state = { name:"", } this.count = 0; } getData=(count)=>{ this.count = count; //依據(jù)異步獲取數(shù)據(jù) setTimeout(()=>{ this.setState({ name:"異步獲取回來的數(shù)據(jù)"+count, }) },200) } componentDidUpdate(prevProps, prevState, snapshot) { console.log("渲染次數(shù),",++count,"次") } render() { return ( <div> <button onClick={()=>this.getData(++this.count)}>點擊獲取數(shù)據(jù)</button> {this.state.name} </div> ) } }
這樣的操作并不會影響我們對它的使用。 在class
組件中我們可以把數(shù)據(jù)存儲在this
上面,而在Function
中,則我們可以通過利用 useRef
這個 Hooks
來實現(xiàn)同樣的效果。
export default function Father1 (){ let [name,setName] = React.useState(''); const countContainer = React.useRef(0); const getData=(count)=>{ //依據(jù)異步獲取數(shù)據(jù) setTimeout(()=>{ setName("異步獲取回來的數(shù)據(jù)"+count) countContainer.current = count++; },200) } return ( <div> <button onClick={()=>getData(++countContainer.current)}>點擊獲取數(shù)據(jù)</button> {name} </div> ) }
場景三:通過存入useRef的數(shù)據(jù)中,避免父子組件的重復(fù)刷新
假設(shè)父組件中有需要用到子組件的數(shù)據(jù),子組件需要把數(shù)據(jù)回到返回給父組件,而如果父組件把這份數(shù)據(jù)存入到了 stat
e 中,那么父組件刷新,子組件也會跟著刷新。 這種的情況我們就可以把數(shù)據(jù)存入到 useRef
中,以避免無意義的刷新出現(xiàn)?;蛘甙褦?shù)據(jù)存入到class的 this
下。
四:合并 state,減少重復(fù) setState 的操作
合并 state
,減少重復(fù) setState
的操作,實際上 React
已經(jīng)幫我們做了,那就是批量更新,在React18
之前的版本中,批量更新只有在 React自己的生命周期或者點擊事件中有提供,而異步更新則沒有,例如setTimeout
,setInternal
等。
所以如果我們想在React18
之前的版本中也想在異步代碼添加對批量更新的支持,就可以使用React
給我們提供的api
。
import ReactDOM from 'react-dom'; const { unstable_batchedUpdates } = ReactDOM;
使用方法如下:
componentDidMount() { setTimeout(()=>{ unstable_batchedUpdates(()=>{ this.setState({ number:this.state.number + 1 }) console.log(this.state.number) this.setState({ number:this.state.number + 1}) console.log(this.state.number) this.setState({ number:this.state.number + 1 }) console.log(this.state.number) }) }) }
五:如何更快的完成diff的比較,加快進程
diff
算法就是為了幫助我們找到需要更新的異同點,那么有什么辦法可以讓我們的diff
算法更快呢?
那就是合理的使用key
diff
的調(diào)用是在reconcileChildren
中的reconcileChildFibers
,當沒有可以復(fù)用current
fiber
節(jié)點時,就會走mountChildFibers
,當有的時候就走reconcileChildFibers
。
而reconcilerChildFibers
的函數(shù)中則會針render
函數(shù)返回的新的jsx
數(shù)據(jù)進行判斷,它是否是對象,就會判斷它的newChild.$$typeof
是否是REACT_ELEMENT_TYPE
,如果是就按單節(jié)點處理。 如果不是繼續(xù)判斷是否是REACT_PORTAL_TYPE
或者REACT_LAZY_TYPE
。
繼續(xù)判斷它是否為數(shù)組,或者可迭代對象。
而在單節(jié)點處理函數(shù)reconcileSingleElement
中,會執(zhí)行如下邏輯:
- 通過
key
,判斷上次更新的時候的Fiber
節(jié)點是否存在對應(yīng)的DOM
節(jié)點。 如果沒有 則直接走創(chuàng)建流程,新生成一個 Fiber 節(jié)點,并返回 - 如果有,那么就會繼續(xù)判斷,
DOM
節(jié)點是否可以復(fù)用?
- 如果有,就將上次更新的
Fiber
節(jié)點的副本作為本次新生的Fiber
節(jié)點并返回
- 如果沒有,那么就標記
DOM
需要被刪除,新生成一個Fiber
節(jié)點并返回。
function reconcileSingleElement( returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement ): Fiber { const key = element.key; //jsx 虛擬 DOM 返回的數(shù)據(jù) let child = currentFirstChild;//當前的fiber // 首先判斷是否存在對應(yīng)DOM節(jié)點 while (child !== null) { // 上一次更新存在DOM節(jié)點,接下來判斷是否可復(fù)用 // 首先比較key是否相同 if (child.key === key) { // key相同,接下來比較type是否相同 switch (child.tag) { // ...省略case default: { if (child.elementType === element.type) { // type相同則表示可以復(fù)用 // 返回復(fù)用的fiber return existing; } // type不同則跳出switch break; } } // 代碼執(zhí)行到這里代表:key相同但是type不同 // 將該fiber及其兄弟fiber標記為刪除 deleteRemainingChildren(returnFiber, child); break; } else { // key不同,將該fiber標記為刪除 deleteChild(returnFiber, child); } child = child.sibling; } // 創(chuàng)建新Fiber,并返回 ...省略 }
從上面的代碼就可以看出,React
是如何判斷一個 Fiber
節(jié)點是否可以被復(fù)用的。
- 第一步:判斷
element
的key
和fiber
的key
是否相同
- 如果不相同,就會創(chuàng)建新的
Fiber
,并返回
- 第二步:如果相同,就判斷
element.type
和fiber
的type
是否相同,type
就是他們的類型,比如p
標簽就是p,div
標簽就是div
.如果type
不相同,那么就會標識刪除。
- 如果相同,那就可以可以判斷可以復(fù)用了,返回
existing
。
而在多節(jié)點更新的時候,key
的作用則更加重要,React
會通過遍歷新舊數(shù)據(jù),數(shù)組和鏈表來通過按個判斷它們的key
和 type
來決定是否復(fù)用。
所以我們需要合理的使用key
來加快diff
算法的比對和fiber
的復(fù)用。
那么如何合理使用key
呢。
其實很簡單,只需要每一次設(shè)置的值和我們的數(shù)據(jù)一直就可以了。不要使用數(shù)組
的下標,這種key
和數(shù)據(jù)沒有關(guān)聯(lián),我們的數(shù)據(jù)發(fā)生了更新,結(jié)果 React
還指望著復(fù)用。
還有哪些工具可以提升性能呢?
實際的開發(fā)中還有其他的很多場景需要進行優(yōu)化:
- 頻繁輸入或者滑動滾動的防抖節(jié)流
- 針對大數(shù)據(jù)展示的虛擬列表,虛擬表格
- 針對大數(shù)據(jù)展示的時間分片 等等等等 后面再補充吧!
到此這篇關(guān)于React 性能優(yōu)化方法總結(jié)的文章就介紹到這了,更多相關(guān)React 性能優(yōu)化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React調(diào)度系統(tǒng)Scheduler工作原理詳解
這篇文章主要為大家介紹了React調(diào)度系統(tǒng)Scheduler工作原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03react中實現(xiàn)搜索結(jié)果中關(guān)鍵詞高亮顯示
這篇文章主要介紹了react中實現(xiàn)搜索結(jié)果中關(guān)鍵詞高亮顯示,使用react實現(xiàn)要比js簡單很多,方法都是大同小異,具體實現(xiàn)代碼大家跟隨腳本之家小編一起看看吧2018-07-07