React是一個UI層面的庫,它採用虛擬DOM技術減少Javascript與真正DOM的互動,提升了前端效能;採用單向資料流機制,父元件通過props
將資料傳遞給子元件,這樣讓資料流向一目瞭然。一旦元件的props
或則state
發生改變,元件及其子元件都將重新re-render和vdom-diff,從而完成資料的流向互動。但是這種機制在某些情況下比如說資料量較大的情況下可能會存在一些效能問題。下面就來分析react的效能瓶頸,並用結合著react-addons-perf
工具來說明react元件拆分的重要性。
react效能瓶頸
要了解react的效能瓶頸,就需要知道react的渲染流程。它的渲染可以分為兩個階段:
初始元件化
該階段會執行元件及其所有子元件的render
方法,從而生成第一版的虛擬dom。元件更新渲染
。
元件的props
或者state
任意發生改變就會觸發元件的更新渲染。預設情況下其也會執行該元件及其所有子元件的render方法獲取新的虛擬dom。
我們說的效能瓶頸指的是元件更新階段的情況。
react元件更新流程
通過上面分析可以知道元件更新具體過程如下:
- 執行該元件及其所有子元件的render方法獲取更新後的虛擬DOM,即
re-render
,即使子元件無需更新。 - 然後對新舊兩份虛擬DOM進行diff來進行元件的更新
在這個過程中,可以通過元件的shouldComponentUpdate
方法返回值來決定是否需要re-render。
react的整個更新渲染流程可以借用一張圖來加以說明:
預設地,元件的shouldComponentUpdate
返回true,即React預設會呼叫所有元件的render方法來生成新的虛擬DOM, 然後跟舊的虛擬DOM比較來決定元件最終是否需要更新。
react效能瓶頸
借圖說話,例如下圖是一個元件結構tree,當我們要更新某個子元件的時候,如下圖的綠色元件(從根元件傳遞下來應用在綠色元件上的資料發生改變):
理想情況下,我們只希望關鍵路徑上的元件進行更新,如下圖:
但是,實際效果卻是每個元件都完成re-render
和virtual-DOM diff
過程,雖然元件沒有變更,這明顯是一種浪費。如下圖黃色部分表示浪費的re-render和virtual-DOM diff。
根據上面的分析,react的效能瓶頸主要表現在:
對於
props
和state
沒有變化的元件,react也要重新生成虛擬DOM及虛擬DOM的diff。
用shouldComponentUpdate
來進行效能優化
針對react的效能瓶頸,我們可以通過react提供的shouldComponentUpdate
方法來做點優化的事,可以有選擇的進行元件更新,從而提升react的效能,具體如下:
shouldComponentUpdate
需要判斷當前屬性和狀態是否和上一次的相同,如果相同則不需要執行後續生成虛擬DOM及其diff的過程,否則需要更新。
具體可以這麼顯示實現:
1 2 3 |
shouldComponentUpdate(nextProps, nextState){ return !isEqual(nextProps, this.props) || !isEqual(nextState, this.state) } |
其中,isEqual方法為判斷兩個物件是否相等(指的是其物件內容相等,而不是全等)。
通過顯示覆蓋shouldComponentUpdate
方法來判斷元件是否需要更新從而避免無用的更新,但是若為每個元件新增該方法會顯得繁瑣,好在react提供了官方的解決方案,具體做法:
方案對元件的
shouldComponentUpdate
進行了封裝處理,實現對元件的當前屬性和狀態與上一次的進行淺對比,從而決定元件是否需要更新。
react在發展的不同階段提供兩套官方方案:
PureRenderMin
一種是基於ES5的React.createClass
建立的元件,配合該形式下的mixins
方式來組合PureRenderMixin提供的shouldComponentUpdate
方法。當然用ES6建立的元件也能使用該方案。
1 2 3 4 5 6 |
import PureRenderMixin from 'react-addons-pure-render-mixin'; class Example extends React.Component { constructor(props) { super(props); this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); } |
PureComponent
該方案是在React 15.3.0版本釋出的針對ES6
而增加的一個元件基類:React.PureComponent
。這明顯對ES6方式建立的元件更加友好。
1 2 3 4 5 6 |
import React, { PureComponent } from 'react' class Example extends PureComponent { render() { // ... } } |
需要指出的是,不管是PureRenderMin
還是PureComponent
,他們內部的shouldComponentUpdate
方法都是淺比較(shallowCompare
)props
和state
物件的,即只比較物件的第一層的屬性及其值是不是相同。例如下面state物件變更為如下值:
1 2 3 |
state = { value: { foo: 'bar' } } |
因為state的value被賦予另一個物件,使nextState.value
與this.props.value
始終不等,導致淺比較通過不了。在實際專案中,這種巢狀的物件結果是很常見的,如果使用PureRenderMin
或者PureComponent
方式時起不到應有的效果。
雖然可以通過深比較方式來判斷,但是深比較類似於深拷貝,遞迴操作,效能開銷比較大。
為此,可以對元件儘可能的拆分,使元件的props
和state
物件資料達到扁平化,結合著使用PureRenderMin
或者PureComponent
來判斷元件是否更新,可以更好地提升react的效能,不需要開發人員過多關心。
元件拆分
元件拆分
,在react中就是將元件儘可能的細分,便於複用和優化。拆分的具體原則:
儘量使拆分後的元件更容易判斷是否更新
這不太好理解,舉個例子吧:假設我們定義一個父元件,其包含了5000個子元件。有一個輸入框輸入操作,每次輸入一個數字,對應的那個子元件背景色變紅。
1 2 3 4 5 6 7 8 9 |
<div> <input value={this.state.inputText} onChange={this.inputChanged}/> <ul { this.state.items.map(el=> <li key={el.id} style={{background: index===this.state.inputText? 'red' : ''}}>{el.name}</li> } </ul> </div> |
本例中,輸入框元件和列表子元件有著明顯的不同,一個是動態的,輸入值比較頻繁;一個是相對靜態的,不管input怎麼輸入它就是5000項。輸入框每輸入一個數字都會導致所有元件re-render,這樣就會造成列表子元件不必要的更新。
可以看出,上面列表元件的更新不容易被取消,因為輸入元件和列表子元件的狀態都置於父元件state中,二者共享;react不可能用shouldComponentUpdate
的返回值來使元件一部分元件更新,另一部分不更新。 只有把他們拆分為不同的元件,每個元件只關心對應的props
。拆分的列表元件只關心自己那部分屬性,其他元件導致父元件的更新在列表元件中可以通過判斷自己關心的屬性值情況來決定是否更新,這樣才能更好地進行元件優化。
儘量使拆分元件的props和state資料扁平化
這主要是從元件優化的角度考慮的,如果元件不需過多關注效能,可以忽略。
拆分元件之所以扁平化,是因為React提供的優化方案PureRenderMin
或者PureComponent
是淺比較元件的props
和state
來決定是否更新元件。
上面的列表元件中,this.state.items存放的是物件陣列,為了更好的判斷每項列表是否需要更新,可以將每個li
列表項單獨拆分為一個列表項元件,每個列表項相關的props就是items陣列中的每個物件,這種扁平化資料很容易判斷是否資料發生變化。
元件拆分的一個例子
為了這篇文章專門寫了一個有關新增展示Todo列表的事例庫。克隆程式碼到本地可以在本地執行效果。
該事例庫是一個有著5000項的Todo列表,可以刪除和新增Todo項。該事例展示了元件拆分前和拆分後的體驗對比情況,可以發現有效能明顯的提升。
下面我們結合react的效能檢測工具react-addons-perf
來說明元件拆分的情況。
拆分前的元件TodosBeforeDivision
的render部分內容如下:
1 2 3 4 5 6 7 8 9 10 |
<input value={this.state.value} onChange={this.inputChange.bind(this)}/> <button onClick={this.addTodo.bind(this)}>add todo</button> { this.state.items.map(el=>{ return ( <TodoItem key={el.id} item={el} tags={['important', 'starred']} deleteItem={this.deleteItem.bind(this, el.id)}/>) }) } |
元件拆分前,輸入框輸入字元、增加todo或者刪除todo項可以看出有明顯的卡頓現象,如下圖所示:
為了弄清楚是什麼原因導致卡頓現象,我們使用chrome的devTool來定位,具體的做法是使用最新版的chrome瀏覽器的Performance
選項來完成。先點選該選項中的record
按鈕開始記錄,這時我們在元件輸入框輸入一個字元,然後點選stop
來停止記錄,我們會看到元件從輸入開始到結束這段時間內的一個效能profile。
從圖可以看出我們在輸入單個字元時,輸入框的input事件邏輯幾乎佔據整個響應時間,具體的處理邏輯主要是react層面的batchedUpdates
方法批量更新列表元件,而不是使用者自定義的邏輯。
那麼,批量更新為啥佔據這麼多時間呢,為了搞清楚原因,我們藉助基於react-addons-perf
的chrome外掛chrome-react-perf
,它以chrome外掛的形式輸出分析的結果。
使用該外掛需要注意一點的是:
chrome-react-perf
外掛的使用需要在專案中引入react-addons-perf
模組,並必須將其物件掛載到window
全域性物件的Perf
屬性上,否則不能使用。
在devTool工具中選擇Perf
選項試圖,點選start
按鈕後其變成stop
按鈕,在元件輸入框中輸入一個字元,然後點選Perf
試圖中的stop
按鈕,就會得出對應的效能試圖。
上圖提供的4個檢視中,Print Wasted
對分析效能最有幫組,它表示元件沒有變化但是參與了更新過程,即浪費了re-render和vdom-diff這一過程,是毫無意義的過程。從圖可以看出:TodosBeforeDivision
和TodoItem
元件分別浪費了167.88ms、144.47ms,這完全可以通過拆分元件避免的開銷,這是react效能優化重點。
為此我們需要對TodosBeforeDivision
元件進行拆分,拆分為一個帶有input和button的動態元件AddTodoForm
和一個相對靜態的元件TodoList
。二者分別繼承React.PureComponent
可以避免不必要的元件更新。
1 2 3 4 5 6 7 8 9 10 11 12 |
export default class AddTodoForm extends React.PureComponent{ ... render(){ return ( <form> <input value={this.state.value} onChange={this.inputChange}/> <button onClick={this.addTodo}>add todo</button> </form> ) } ... } |
其中TodoList
元件還需要為每項Todo任務拆分為一個元件TodoItem
,這樣每個TodoItem
元件的props
物件為扁平化的資料,可以充分利用React.PureComponent
來進行物件淺比較從而更好地決定元件是否要更新,這樣避免了新增或者刪除一個TodoItem
項時,其他TodoItem
元件不必更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
export default class TodoList extends React.PureComponent{ ... render(){ return ( <div> {this.props.initailItems.map(el=>{ return <TodoItem key={el.id} item={el} tags={this.props.tags} deleteItem={this.props.deleteItem}/> })} </div> ) } ... } export default class TodoItem extends React.PureComponent{ ... render(){ return ( <div> <button style={{width: 30}} onClick={this.deleteItem}>x</button> <span>{this.props.item.text}</span> {this.props.tags.map((tag) => { return <span key={tag} className="tag"> {tag}</span>; })} </div> ) } ... } |
這樣拆分後的元件,在用上面的效能檢測工具檢視對應的效果:
從上面的截圖可以看出,拆分後的元件效能有了上百倍的提升,雖然其中還包含一些其他優化,例如不將function在元件屬性位置繫結this以及常量物件props快取起來等避免每次re-render時重新生成新的function和新的物件props。
總的來說,對react元件進行拆分對react效能的提升是非常重要的,這也是react效能優化的一個方向。