一個用於解決 React 常見問題的 Checklist
這是一份非常實用的,一步步解決 React
效能問題的清單。
你想不想讓你的 React
應用程式執行更快?
想不想有一份清單來檢查常見的 React
效能問題?
如果你的答案,都是 yes~那就抓緊時間來讀一下這篇文章。這是一份非常實用的解決 React
效能問題的清單
在本文中,我將為大家介紹一個逐步消除常見效能問題的指南。
我會先描述問題,然後再給出解決方案。在這同時,你可以將同樣的問題帶入到你自己的專案中審查。
下面就開始動手吧~
專案
為了大家都方便學習,我會在同一個 React
應用上來介紹各種問題的情況。
專案名稱為 Cardie
可以在 Github
上下載Cardie
原始碼,跟著我一起學習。
Cardie
是一個功能非常簡單的專案。就是常見的個人簡介頁,用來展示一個使用者的個人介紹。
頁面內包含一個可點選的按鈕,可以操作更改使用者的職業資訊。
Cardie in Action ?
點選按鈕,使用者的職業資訊會發生改變。
看到這裡,你可能會有點忍不住想笑了。這個應用相比於真實的專案也太過於簡單了。這個的專案的效能問題,能和我們真實世界的應用比嗎?
現在,我們繼續。
1. 辨別無用的渲染
解決效能問題第一步,最好的方式就是從分辨哪些是你專案裡,沒有用的渲染。
檢查方式多種多樣,但最簡單的方式,就是開啟你的React devtools
面板,切換 highlight updates
如何在 React devtools
中開啟 highlight updates
這個時候,App 上更新渲染的地方,會有一個閃爍。
在蒙層下面的元件,會被 React
重新渲染。
點選切換職業,會發現,整個父級 App
都被重新渲染了。
注意使用者卡片邊緣的閃爍
這就不太對了。
可以看到 App
雖然執行正常,但是更改很小的地方,不應該整個元件都重新渲染啊。
實際發生改變的是 App
內很小的一部分
理想的更新應該像下面這樣:
注意更新只發生在很小的區域內
無用的重新渲染對於複雜的專案來說,更是會引發不小的效能問題。
發現問題了嗎?解決了嗎?
2. 將需要頻繁更新的區域單獨建立為元件
一旦在你的應用裡,發現了無用的渲染,重新去整理你的元件樹,是非常好的解決方式。
下面我們來詳細說明一下。
在 Cardie
中,App
元件通過 react-redux
裡的 connect
方法連線到 redux store
。從 store
中獲取屬性:name
, location
, likes
和 description
。
<App/>
直接操作 redux store
獲取資料
在個人介紹頁目前定義了 description
屬性。
原因就是因為,點選按鈕的時候, description
屬性發生了改變。改變引發了 App
元件的重新渲染。
記不記得 React 101
裡有句話,一個元件中無論是 props
還是 state
發生改變,都會觸發重新渲染。
React 元件的元素渲染樹。這些元素是通過 props
和 state
定義的。如果 props
或者 state
發生改變,元素樹會重新渲染。結果就會是一個新的樹。
我們應該如何讓一個特定的 React
元件元素更新,而不是 App
元件?
例如: 我們可以建立一個新元件,名字叫 Profession
。渲染它自己的元素。
在這個例子裡,如果將職業點選切換為“我是一個程式設計師”的時候, Profession
元件會被重新渲染。
在 元件內的 元件會重新渲染
新的元件樹如圖:
元件渲染了包括 元件的元素
也就是說之前是 元件在關心 profession
屬性,現在變成了 <Profession/>
這個元件的事情。
元件 <Profession/>
會直接從 redux store
獲取 profession
屬性。
不管你是否使用了 redux
,這裡的關鍵點是 App
元件不再會因為 profession
屬性的改變,而引發重繪。而是被元件 <Profession/>
取代了。
更改之後,我們再來看一下 update highlighted
的表現:
注意只有 <Profession />
內部發生了更新
3. 在恰當的地方使用靜態元件
React
提到效能問題,不得不說的就是靜態元件。那麼,怎樣正確使用靜態元件
當然,你可以把每個元件都寫成靜態元件,但是你要記得,有一個方法特殊。 shouldComponentUpdate
方法。
我們假定只有 props
和之前的 props
和 state
不相同的時候,會引發元件的重新渲染。
和 React.PureComponent
相對的就是預設的 React.Component
元件。
用 React.PureComponent
替代 React.Component
為了解釋 Cardie
專案裡,對於這個具體使用的區別,
我們將 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
屬性,它傳遞這個屬性值給到 Profession
元件。從理論上來說,其他三個元件不需要關心 profession
的值。
我們假設新的子元件內容是非常簡單的。<I />
元件 就返回一個 span
元素,內部包裹著一個字母 I
, <span >I </span>
。
專案可以正常執行,結果也沒有問題。
但是當你修改 description
這個屬性的時候,Profession
下所有的子元件都跟著重新渲染了
一旦有新的屬性值傳遞給 Description
元件時,其他子元件也會被重新渲染。
我在render
方法里加了日誌輸出,我們可以真實的在控制檯看到,每個子元件都被重新渲染了。
你也可以在 react dev tools
裡檢視 highlighted updates
。
注意這幾個詞 I, am
和 a.
都被重新渲染過
這是意料之中的事情。無論是 props
還是 state
發生改變,元素樹都會被重排。
和重繪是一樣的。
在這個例子裡,我們需要提前約定好, <I/>, <Am/>
和 <A/>
三個元件是不需要重繪的。沒錯, props
來自父元件,<Description/>
變化了。這是不可避免的。但是假設我們的應用足夠大,這個現象就會引發構成效能威脅。
假設我們使用靜態元件作為子元件呢?
<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>;
}
補充說明一下, React
通過 hood
通知到這些子元件,即使屬性值有變化,他們也不需要更新渲染。
就是說無論父元素屬性值如何變化,都不重新渲染!
瞭解原理之後,我們來看 highlighted updates
, 如圖所示,子元件不再重新渲染了。
prop
值變化時,只有 Profession
元件重新渲染了。
在字母 “I”, “am” and “a”.
這三個周圍沒有出現邊框,只有包裹的容器,profession
元件閃爍。
在更大型的應用裡,通過設定為靜態元件,可能可以大幅度提升效能。
4. 避免給 props
傳新物件
再次強調一下,props
變化,會導致元件重新渲染。
不管是 props
還是 state
發生變化,VDOM 樹都會重新渲染,導致生成新的 VDOM 樹
萬一你的元件沒改變 props
, 但 React
認為它改變了呢?
是的,也會重新渲染!
但是這不是很奇怪嗎?
這種情況的發生是和 javascript
的執行原理, React
處理新舊值比較的實現方式是有關係的。
讓我們看一個例子。
下面是 Description
元件的程式碼內容:
const Description = ({ description }) => {
return (
<p>
<I />
<Am />
<A />
<Profession profession={description} />
</p>
);
};
接下來,我們給 I
元件定義一個 i
屬性值。定義為一個物件:
const i = {
value: "i"
};
不管 value 裡,具體值是什麼,我們都可以直接取變數名渲染到元件 I
中。
class I extends PureComponent {
render() {
return <span className="faint">{this.props.i.value} </span>;
}
}
在元件 Description
中,屬性 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/>
和 <Profession/>
元件都重新渲染了。但是<I/>
元件真實的屬性並沒有變化啊,為什麼會這樣?
這是為什麼呢?
Description
元件接收了一個新的屬性值, render
方法被呼叫生成了新的 VDOM
樹。
為了給 render
方法傳參,定義了一個新常量 i
:
const i = {
value: "i"
};
當 React
執行這行程式碼的時候,<I i={i} />
,它將 i
當作一個不同的屬性,一個新的物件進行傳參的,所以重新渲染了。
如果你記得 React 101
, React
會對新舊 props
進行對比。
像字串和數字這種值型別的,是針對值來進行對比。物件型別是引用對比。
儘管常量 i
每次都是渲染相同的值,但是引用地址不同,記憶體不記錄它上次的位置。
每次渲染都會被新建物件,所以, prop
每次傳給 <I />
元件的都是”新“ 的物件,所以不斷的重新渲染。
在一個大型應用場景中,這會導致無用渲染,導致潛在的效能陷阱。
如何避免呢?
可以給每一個 prop
加一個事件處理。
如果你想避免這種情況的發生,你就不能這樣寫:
...
render() {
<div onClick={() => {//do something here}}
}
...
你可以新建一個方法物件,每次渲染的時候去呼叫方法。像這樣:
...
handleClick:() ={
}
render() {
<div onClick={this.handleClick}
}
...
明白了嗎?
同樣,我們需要把 prop
傳遞給 <I>
class Description extends Component {
i = {
value: "i"
};
render() {
return (
<p>
<I i={this.i} />
<Am />
<A />
<Profession profession={this.props.description} />
</p>
);
}
}
這樣的話,引用就是同一個了, this.i
。
渲染的時候就不會有新物件了。
5. 使用構建工具
當你需要發生產環境的時候,要使用構建工具。使用很簡單,效果很顯著。
development build
是在提醒你, react
開發者工具在開發環境使用的
如果你是用 create-react-app
來構建你的應用的,可以直接執行命令 npm run build
。
這樣會打包出適合線上環境的優化後的檔案。
6. 使用程式碼分割
當你打包你的應用的時候,你很可能會得到一個很大的檔案。
隨著你應用的複雜程度的增加, bundle 也會越來越大。
一旦使用者訪問網頁,就會接收到整個 app
的全部程式碼。
程式碼分割是指,將程式碼分成一部分一部分,當用戶需要的時候,再去請求,而不是一下子全部返回給使用者。
最常見的例子就是路由的程式碼分割。在這種方式下,應用會根據路由變化,返回對應的程式碼片段。
/home
會看的一部分程式碼,/about
會看到一部分。
另一個應用場景就是基於元件的程式碼分割。在這種情況下,給使用者展示的就不是一個元件,而是延遲將元件傳送給使用者。
這裡最重要的是理解和權衡應用功能和使用者體驗之間的關係,採用用哪種方式並不重要。
程式碼分割的方式是可以提供你的應用的效能的。
這裡我就不展開來說了,如果你對程式碼分割感興趣,你可以參考官方文件 React docs.
。他們給出的解釋更專業。
結論
現在你有了解決 react
應用的效能問題,常用的跟蹤和修復問題的方法,快去試試讓你的App
體驗更快吧!