原文 Death by a thousand cuts - a checklist for eliminating common React performance issues
github的地址 歡迎star
前言(可以略過)
我們今天將會用一個具體的例子一步步的解決React的一些常見的效能問題。 你想知道怎樣讓你的React專案執行更快嗎? 如果是,請繼續往下看! 此時如果有一份常見的React效能優化清單是多麼方便,沒錯,在這裡就有一份!
首先,我會直接給你看專案中問題,並給出問題相應的解決方法。這樣做,就和我們實際上的專案差別不大了(在一些概念上)。
這篇文章並不是長篇大論,相反地,我們討論一些東西都是今後你們馬上就能用到的。
一個示例專案
為了是文章講的儘可能真實,我通過這個簡單的React應用(名字叫 Cardie的app)帶你經歷各種實際的使用者場景。
叫 Cardie 的手機應用 github原始碼地址Cardie僅展示了使用者資訊,通常稱呼為使用者檔案卡。當然,它也提供一個按鈕,點選改變使用者的一些資訊。
點選app底部的按鈕後,使用者的資訊就會被改變儘管它如此的簡單,一點都不是app,但當你通過這個例子尋找並解決效能問題之後,真實環境中App的效能問題你也能隨之解決。 請保持耐心與放鬆。下面正式介紹優化清單!
1. 辨別無用浪費的渲染
辨別無用浪費的渲染,是這份清單的一個良好開端。 有幾種不同的方法解決辨別問題,最簡單方法,通過React dev tools工具實現,點選設定按鈕,勾選“highlight updates”選項,就可以清楚看到哪些元件更新了。
當使用者和app互動的時候,在螢幕上更新的元件就會出現綠色的閃光光罩。對於Cardie,改變使用者資訊後,似乎整個App元件都重新渲染了
注意環繞使用者卡片資訊的綠色的閃光光罩。這似乎是不應該的。
當app執行的時候,只改變元件一小點地方,不應該導致整個元件的的重新渲染。
實際更新應該發生在App元件的一小部分更理想的高亮更新顯示應該是這個樣子:
注意現在的高亮更新只顯示包含了一小塊更新的區域在更復雜的應用中,無效浪費渲染的影響是巨大的!解決了重新渲染問題足夠提升應用的效能了。
看完這個問題,對此你有什麼解決辦法?
2. 將需要頻繁更新的區域抽離成獨立的元件
一旦你注意到了你應用中無用的渲染,從你的元件樹中拆分出頻繁更新的區域是一個不錯的開始。
下面具體說明如何拆分。
在 Cardie中,App
元件通過react-redux
中connect
函式連線到了redux store
。從store
中,它接受的props
有:name
,location
,likes
以及description
。
<App/>
需要直接從redux store
中接受的props
description
的props定義了當前使用者的資訊。
本質上,無論何時點選按鈕改變了使用者資訊,description
的值都會改變。它的改變導致了整個App
元件的重新渲染。
一個react元件會渲染一個由你是否想起了React官網中說的,每當元件的
props
或state
改變時,都會觸發該元件重新渲染。
props
和state
元素定義的元件樹。如果props
或state
改變,這個狀態元件樹就會重新生成一個新的樹
此時我們要把需要更新的元件單獨拆分出來渲染,而不是整個App
元件毫無意義的重新渲染。
例如,我們可以建立一個叫Profession
元件渲染自己的DOM元素。
這樣的話,Profession
元件就能渲染使用者資訊的description 的資料,列如I am a Coder
<Profession/>
在<App/>
中渲染
此時元件樹看起來是這樣:
對於profession
的props,我們關注的重點不再是<App/>
,而是變成了<Profession/>
profession
資料由<Profession/>
元件直接從redux store
獲取
不管你是否使用redux
,這時改變profession
的props,將不再導致App
的重新渲染,而<Profession/>
將重新渲染。
完成這個重構之後,你將得到理想的高亮更新顯示:
想在高亮更新僅包含<Profession />
為了檢視程式碼的更改,請從遠端倉庫克隆切換分支到 isolated-component branch檢視
3. 適當地使用純元件
任何提到React效能的地方都會提及到pure components
。
然而,你知道怎樣在合適的時候使用純元件嗎?
當然,我們可以把每個元件都變成純元件,但請記住不要對外層的容器元件這樣操作。因為還有shouldComponentUpdate
函式。
純元件沒有自己的state,因此,純元件的重新渲染僅僅由props
改變導致的。
一個簡單的實現純元件的方法是用React.PureComponent
代替React.Component
React.PureComponent
代替React.Component
用插畫展示這個特定的用法,把Profession
拆分成粒度更小的元件。
這是拆分之前的Profession
元件
const Description = ({ description }) => {
return (
<p>
<span className="faint">I am</span> a {description}
</p>
);
}
複製程式碼
這是拆分之後的元件:
const Description = ({ description }) => {
return (
<p>
<I />
<Am />
<A />
<Profession profession={description} />
</p>
);
};
複製程式碼
現在Description
元件就渲染4個子元件。
Description
元件有profession
的prop,這個prop只傳遞給了Profession
,其它3個元件並不關心profession
的值。
當然這些新元件的內容是極其簡單的。例如,<I />
元件僅僅渲染了一個span >I </span>
元素。
有趣的是每當description
的prop改變之後,Profession
的每個子元件都會重新渲染。
Description
接受一個新的prop值,它的所有子元件都會重新渲染
我在每個子元件的render
方法新增了logs
日誌,你能夠確實看到每個子元件都被重新渲染了。
你可以用 react dev tools檢視高亮更新部分
這個行為是符合預期的。每當元件的props或者state改變,元件渲染的樹就會重新計算。
在這個例子中,你應該認同讓元件<I/>, <Am/> 以及 <A/>
重新渲染是沒有任何意義的。尤其是在你的專案足夠的大的時候,這將產生效能問題。
那麼怎麼把子元件變成純元件呢?
針對<I/>
元件
import React, {Component, PureComponent} from "react"
//before
class I extends Component {
render() {
return <span className="faint">I </span>;
}
//after
class I extends PureComponent {
render() {
return <span className="faint">I </span>;
}
複製程式碼
這樣修改後,當這些子元件中prop的值沒有改變時,react就不會重新渲染這些元件。
對於這個例子,即使你改變了props的值,<I/>, <Am/> 以及 <A/>
這些元件也不會重新渲染!
在重構之後觀察高亮更新的顯示,你會發現<I/>, <Am/> 以及 <A/>
這些元件都沒有更新,僅僅Profession
元件改變了,因為它的prop改變了。
<I/>, <Am/> 以及 <A/>
元件沒有顯示高亮更新
在大型專案中,把某些元件改成純元件你會發現巨大的效能提升。
為了檢視程式碼的更改,請從遠端倉庫克隆切換分支到 pure-component branch檢視
4. 避免通過一個新的物件作為props
重複一遍,每當一個元件的props改變時,就會重新渲染這個元件。
當props或者state改變時,元件tree就會返回一個新的
如果你的元件的props
沒有改變,但React認為它改變了,會怎樣呢?
它們也會重新渲染!
是不是困惑的?
出現這種異常情況,你需要知道JavaScript是怎麼工作的,以及React是怎麼對比舊的和新的prop值的變化的。
來看看這個例子。
Description
元件的內容:
const Description = ({ description }) => {
return (
<p>
<I />
<Am />
<A />
<Profession profession={description} />
</p>
);
};
複製程式碼
接下來,我們會重構I
元件,給他傳入了命名為i
的prop,作為一個表單提交的屬性物件:
class I extends PureComponent {
render() {
return <span className="faint">{this.props.i.value} </span>;
}
}
複製程式碼
在Description
元件中,以如下的方式建立了i
並傳遞給I
元件:
class Description extends Component {
render() {
const i = {
value: "i"
};
return (
<p>
<I i={i} />
<Am />
<A />
<Profession profession={this.props.description} />
</p>
);
}
}
複製程式碼
接下來請耐心聽我的解釋,
這是完全錯誤的程式碼,但它能夠正常執行,除了有一個問題。
儘管I
元件是一個純元件,但只要使用者的職業這個屬性改變了就會重新渲染I
。
<I/>
and<Profession/>
元件都重新渲染了。事實上,props並沒有發生改變,為什麼重新渲染了呢?
為什麼?
一旦Description
元件接受了一個新的props,就會呼叫render
生成一個React的元件樹。
在呼叫render
函式之後,ta就會建立一個新的i
的例項:
const i = {
value: "i"
};
複製程式碼
當React執行到<I i={i} />
這裡的時候,它認為i已經變了,因為它是一個新的物件(引用的物件變了),因此重新渲染了。
React判斷當前的props和新的props過程的是淺比較
基本型別的資料像字串和數字就是比較他們的值,而物件是比較它們的引用是否相等。
即使i的值改變前後是一樣,但它指向的引用物件變了(在記憶體中的位置不相同),所以會重新渲染。
每次呼叫render
,就會新生成一個物件。<I/>
中prop的i
就會被當做新的,導致重新渲染。
在更大的應用中,它就會導致無效的渲染,造成潛在的效能問題。
應該避免這樣。
在應用中prop
也會包含事件處理。
如果要避免效能浪費,那麼不應這樣:
...
render() {
<div onClick={() => {//do something here}}
}
...
複製程式碼
每次render
都會產生一個新的函式物件。應該像這樣:
...
handleClick:() ={
}
render() {
<div onClick={this.handleClick}
}
...
複製程式碼
明白了嗎?
同理,我們如下重構<I />
:
class Description extends Component {
i = {
value: "i"
};
render() {
return (
<p>
<I i={this.i} />
<Am />
<A />
<Profession profession={this.props.description} />
</p>
);
}
}
複製程式碼
現在,在<I i={this.i} />
中i
的引用都是this.i
。呼叫render
就不會產生新的物件。
為了檢視程式碼的更改,請從遠端倉庫克隆切換分支到 new-objects branch檢視
5. 使用生產模式構建打包
部署到生產環境,應該要使用React的生產模式。它是簡單的卻是最好的實踐。
在開發模式下執行構建,點選react開發者工具就會彈出警告如果你使用了create-react-app
的腳手架,執行生產模式打包,僅需執行命令:
npm run build
。
它將會盡可能優化壓縮你的程式碼。
6. 使用程式碼分割(code splitting)
當你打包你的應用,你可能會把整個專案打包成一個大的塊。
此時的問題,當你的應用變大的時候,那個塊也會變大。
當使用者訪問網站,他就會獲取到整個專案的一個大塊程式碼分割提倡的不是使用者立即就獲取到專案的整個塊,而是使用者需要的時候,才動態的載入相關的專案塊。
一個常見的例子就是通過路由來程式碼分割。這個方法,程式碼將會根據路由劃分成不同的塊。
/home
路由被劃分成了一個小塊,/about
路由也一樣
也還有其他程式碼分割的方法。比如,如果一個元件當前對使用者來說是不可見的,那它就可以延遲載入,當使用者需要的時候在渲染。
無論你選擇哪一種方法,重要的是做好權衡,不要降低了使用者體驗。
程式碼分割是極好的,它能提高你應用的效能。
我僅僅從概念上解釋了程式碼分割的知識。如果你想知道更多程式碼分割的知識,請檢視React的官方文件,很好的解釋了程式碼分割的知識。
結尾
現在你已經獲取到一份還算可以的追蹤修復效能問題的清單了。快來讓你的應用更快吧!
廣告略過
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!