本文的敘事線索與代碼示例均來自High Performance Redux,特此表示感謝。之因此感謝是由於最近一直想系統的整理在 React + Redux 技術棧下的性能優化方案,但苦於找不到切入點。在查閱資料的過程當中,這份 Presentation 給了我很大的啓發,它的不少觀點一針見血,也與個人想法不謀而合。因而這篇文章也是參照它的講解線索來依次展開我想表達的知識點javascript
或許你已經據說過不少的第三方優化方案,好比immutable.js
,reselect
,react-virtualized
等等,有關工具的故事下一篇再詳談。首先咱們須要瞭解的是爲何會出現性能問題,以及解決性能問題的思路是什麼。當你瞭解完這一切以後,你會發現其實不少性能問題不須要用第三方類庫解決,只須要在編寫代碼中稍加註意,或者稍稍調整數據結構就能有很大的改觀。前端
每一個人對性能都有本身的理解。其中有一種觀點認爲,在程序開發的初期不須要關心性能,當程序規模變大而且出現瓶頸以後再來作性能的優化。我不一樣意這種觀點。性能不該該是後來居上的補丁,而應該是程序天生的一部分。從項目的第一天起,咱們就應該考慮作一個10x project:即可以運行 10k 個任務而且擁有 10 年壽命java
退一步說即便你在項目的後期發現了瓶頸問題,公司層面不必定會給你足夠的排期解決這個問題,畢竟業務項目依然是優先的(仍是要看這個性能問題有多「痛」);再退一步說,即便容許你展優化工做,通過長時間迭代開發後的項目已經和當初相比面目全非了:模塊數量龐大,代碼耦合嚴重,尤爲是 Redux 項目牽一髮而動全身,再想對代碼進行優化的話會很是困難。從這個意義上來講,從一開始就將性能考慮進產品中去也是一種 future-proof 的體現,提升代碼的可維護性node
從另外一個角度看,代碼性能也是我的編程技藝的體現,一位優秀的程序員的代碼性能應當是有保障的。react
前端框架喜歡把實現 Todo List 做爲給新手的教程。咱們這裏也拿一個 List 舉例。假設你須要實現一個列表,用戶點擊有高亮效果僅此而已。特別的地方在於這個列表有 10k 的行,是的,你沒看錯 10k 行(上面不是說好咱們要作 10x project 嗎:p)git
首先咱們看一看基本款代碼,由App
組件和Item
組件構成,關鍵代碼以下:程序員
function itemsReducer(state = initial_state, action) {
switch (action.type) {
case "MARK":
return state.map(
item =>
action.id === item.id ? { ...item, marked: !item.marked } : item
);
default:
return state;
}
}
class App extends Component {
render() {
const { items, markItem } = this.props;
return (
<div> {items.map(({ id, marked }) => ( <Item key={id} id={id} marked={marked} onClick={markItem} /> ))} </div> ); } } function mapStateToProps(state) { return state; } const markItem = id => ({ type: "MARK", id }); export default connect(mapStateToProps, { markItem })(App); 複製代碼
這段關鍵的代碼體現了幾個關鍵的事實:github
item
)的數據結構是{ id, marked }
items
)的數據結構是數組類型:[{id1, marked}, {id2, marked}, {id3, marked}]
App
渲染列表是經過遍歷(map
)列表數組items
實現的id
傳遞給item
的 reducer,reducer 經過遍歷 items
,挨個對比id
的方式找到須要被標記的項App
,App
再次進行渲染若是你無法將以上代碼片斷和我敘述的事實拼湊在一塊兒,能夠在 github 上找到完整代碼瀏覽或者運行。web
對於這樣的一個需求,相信絕大多數人的代碼都是這麼寫的。chrome
可是上述代碼沒有告訴你的事實時,這的性能不好。當你嘗試點擊某個選項時,選項的高亮會延遲至少半秒秒鐘,用戶會感受到列表響應變慢了。
這樣的延遲值並非絕對:
ENV === 'development'
)下運行的代碼會比在生產環境(ENV === 'production'
)下運行較慢那麼問題出在哪裏?咱們經過 Chrome 開發者工具一探究竟(還有不少其餘的 React 相關的性能工具一樣也能洞察性能問題,好比 react-addons-perf, why-did-you-update,React Developer Tools 等等。但都存在或多或少的存在缺陷,使用 Chrome 開發者工具是最靠譜的)
react_perf
後綴的方式訪問項目頁面,好比個人項目地址是: http://localhost:3000/ 的話,實際請訪問 http://localhost:8080/?react_perf 。加上react_perf
後綴的用意是啓用 React 中的性能埋點,這些埋點用於統計 React 中某些操做的耗時,使用User Timing API
實現咱們把目光聚焦到 CPU 活動最劇烈的那段時間內,
從圖表中能夠看出,這部分的時間(712ms)消耗基本是由腳本引發的,準確來講是由點擊事件執行的腳本引發的,而且從函數的調用棧以及從時間排序中能夠看出,時間基本上花費在updateComponent
函數中。
這已經能猜出一二,若是你還不肯定這個函數究竟幹了什麼,不如展開User Timing
一欄看看更「通俗」的時間消耗
原來時間都花費在App
組件的更新上,每一次App
組件的更新,意味着每個Item
組件也都要更新,意味着每個Item
都要被從新渲染(執行render
函數)
若是你依然以爲對以上說法表示懷疑,或者說不可思議,能夠直接在App
組件的render
函數和Item
組件的render
函數加上console.log
。那麼每次點擊時,你會看到App
裏的console
和Item
裏的console
都調用了 10k 次。注意此時頁面會響應的更慢了,由於在控制檯輸出 10k 次console.log
也是須要代價的
更重要的知識點在於,只要組件的狀態(props
或者state
)發生了更改,那麼組件就會默認執行render
函數從新進行渲染(你也能夠經過重寫shouldComponentUpdate
手動阻止這件事的發生,這是後面會提到的優化點)。同時要注意的事情是,執行render
函數並不意味着瀏覽器中的真實 DOM 樹須要修改。瀏覽器中的真實 DOM 是否須要發生修改,是由 React 最後比較 Virtual Tree 決定的。 咱們都知道修改瀏覽器中的真實 DOM 是很是耗費性能的一件事,因而 React 爲咱們作出了優化。可是執行render
的代價仍然須要咱們本身承擔
因此在這個例子中,每一次點擊列表項時,都會引發 store 中items
狀態的更改,而且返回的items
狀態老是新的數組,也就形成了每次點擊事後傳遞給App
組件的屬性都是新的
請記住下面這個公式
UI = f(state)
你在頁面上所見的,都是對狀態的映射。反過來講,只要組件狀態或者傳遞給組件的屬性沒有發生改變,那麼組件也不會從新進行渲染。咱們能夠利用這一點阻止App
的渲染,只要保證轉遞給App
組件的屬性不會發生改變便可。畢竟只修改一條列表項的數據卻結果形成了其餘 9999 條數據的從新渲染是不合理的。
可是應該如何作才能保證修改數據的同時傳遞給App
的數據不發生變化?
經過更改數據結構
本來全部的items
信息都存在數組結構裏,數組結構的一個重要特性是保證了訪問數據的順序一致性。如今咱們把數據拆分爲兩部分
ids
:只保留 id 用於記錄數據順序,好比:[id1, id2, id3]
items
:以key-value
的形式記錄每一個數據項的具體信息:{id1: {marked: false}, id2: {marked: false}}
關鍵代碼以下:
function ids(state = [], action) {
return state;
}
function items(state = {}, action) {
switch (action.type) {
case "MARK":
const item = state[action.id];
return {
...state,
[action.id]: { ...item, marked: !item.marked }
};
default:
return state;
}
}
function itemsReducer(state = {}, action) {
return {
ids: ids(state.ids, action),
items: items(state.items, action)
};
}
const store = createStore(itemsReducer);
class App extends Component {
render() {
const { ids } = this.props;
return (
<div> {ids.map(id => { return <Item key={id} id={id} />; })} </div> ); } } // App.js: function mapStateToProps(state) { return { ids: state.ids }; } // Item.js function mapStateToProps(state, props) { const { id } = props; const { items } = state; return { item: items[id] }; } const markItem = id => ({ type: "MARK", id }); export default connect(mapStateToProps, { markItem })(Item); 複製代碼
在這種思惟模式下,Item
組件直接與 Store 相連,每次點擊時經過 id 直接找到items
狀態字典中的信息進行修改。由於App
只關心ids
狀態,而在這個需求中不涉及增刪改,因此ids
狀態永遠不會發生改變,在Mounted
以後,App
不再會更新了。因此如今不管你如何點擊列表項,只有被點擊的列表項會更新。
不少年前我寫過一篇文章:《在 Node.js 中搭建緩存管理模塊》,裏面提到過相同的解決思路,有更詳細的敘述
在這一小節的結尾我要告訴你們一個壞消息:雖然咱們能夠精心設計狀態的數據結構,但在實際工做中用來展現數據的控件,好比表格或者列表,都有各自獨立的數據結構的要求,因此最終的優化效果並不是是理想狀態
讓咱們回到最初發生事故的代碼,它的問題在於每次在渲染須要高亮的代碼時,無需高亮的代碼也被渲染了一遍。若是能避免這些無辜代碼的渲染,那麼一樣也是一種性能上的提高。
你確定已經知道在 React 組件生命週期就存在這樣一個函數 shoudlComponentUpdate
能夠決定是否繼續渲染,默認狀況下它返回true
,即始終要從新渲染,你也能夠重寫它讓它返回false
,阻止渲染。
利用這個生命週期函數,咱們限定只容許marked
屬性發生先後發生變動的組件進行從新渲染:
class Item extends Component {
constructor() {
//...
}
shouldComponentUpdate(nextProps) {
if (this.props["marked"] === nextProps["marked"]) {
return false;
}
return true;
}
複製代碼
雖然每次點擊時App
組件仍然會從新渲染,可是成功阻止了其餘 9999 個Item
組件的渲染
事實上 React 已經爲咱們實現了相似的機制。你能夠不重寫shouldComponentUpdate
, 而是選擇繼承React.PureComponent
:
class Item extends React.PureComponent 複製代碼
PureComponent
與Component
不一樣在於它已經爲你實現了shouldComponentUpdate
生命週期函數,而且在函數對改變先後的 props 和 state 作了「淺對比」(shallow comparison),這裏的「淺」和「淺拷貝」裏的淺是同一個概念,即比較引用,而不比較嵌套對象裏更深層次的值。話說回來 React 也沒法爲你比較嵌套更深的值,一方面這也耗時的操做,違背了shouldComponentUpdate
的初衷,另外一方面複雜的狀態下決定是否從新渲染組件也會有複雜的規則,簡單的比較是否發生了更改並不穩當
殘酷的現實是,即便你理解了以上的知識點,你可能仍然對平常代碼中的性能陷阱渾然不知,
好比設置缺省值的時候:
<RadioGroup options={this.props.options || []} />
複製代碼
若是每次 this.props.options
值都是 null
的話,意味着每次傳遞給<RadioGroup />
都是字面量數組[]
,但字面量數組和new Array()
效果是同樣的,始終生成新的實例,因此表面上看雖然每次傳遞給組件的都是相同的空數組,其實對組件來講每次都是新的屬性,都會引發渲染。因此正確的方式應該將一些經常使用值以變量的形式保存下來:
const DEFAULT_OPTIONS = []
<RadioGroup options={this.props.options || DEFAULT_OPTIONS} />
複製代碼
又好比給事件綁定函數的時候
<Button onClick={this.update.bind(this)} />
複製代碼
或者
<Button
onClick={() => {
console.log("Click");
}}
/>
複製代碼
在這兩種狀況下,對於組件來講每次綁定的都是新的函數,因此也會形成從新渲染。關於如何在eslint
中加入對.bind
方法和箭頭函數的檢測,以及解決之道請參考No .bind() or Arrow Functions in JSX Props (react/jsx-no-bind)
下一篇咱們學習如何藉助第三方類庫,好比immutablejs
和reselect
對項目進行優化
這篇文章同時也發表在個人 知乎前端專欄,歡迎你們關注