【譯】精挑細選的一份React效能問題優化的清單

sundjly發表於2019-03-19

原文 Death by a thousand cuts - a checklist for eliminating common React performance issues

github的地址 歡迎star

前言(可以略過)

我們今天將會用一個具體的例子一步步的解決React的一些常見的效能問題。 你想知道怎樣讓你的React專案執行更快嗎? 如果是,請繼續往下看! 此時如果有一份常見的React效能優化清單是多麼方便,沒錯,在這裡就有一份!

首先,我會直接給你看專案中問題,並給出問題相應的解決方法。這樣做,就和我們實際上的專案差別不大了(在一些概念上)。

這篇文章並不是長篇大論,相反地,我們討論一些東西都是今後你們馬上就能用到的。

一個示例專案

為了是文章講的儘可能真實,我通過這個簡單的React應用(名字叫 Cardie的app)帶你經歷各種實際的使用者場景。

【譯】精挑細選的一份React效能問題優化的清單
叫 Cardie 的手機應用 github原始碼地址

Cardie僅展示了使用者資訊,通常稱呼為使用者檔案卡。當然,它也提供一個按鈕,點選改變使用者的一些資訊。

【譯】精挑細選的一份React效能問題優化的清單
點選app底部的按鈕後,使用者的資訊就會被改變

儘管它如此的簡單,一點都不是app,但當你通過這個例子尋找並解決效能問題之後,真實環境中App的效能問題你也能隨之解決。 請保持耐心與放鬆。下面正式介紹優化清單!

1. 辨別無用浪費的渲染

辨別無用浪費的渲染,是這份清單的一個良好開端。 有幾種不同的方法解決辨別問題,最簡單方法,通過React dev tools工具實現,點選設定按鈕,勾選“highlight updates”選項,就可以清楚看到哪些元件更新了。

【譯】精挑細選的一份React效能問題優化的清單
當使用者和app互動的時候,在螢幕上更新的元件就會出現綠色的閃光光罩。

對於Cardie,改變使用者資訊後,似乎整個App元件都重新渲染了

【譯】精挑細選的一份React效能問題優化的清單
注意環繞使用者卡片資訊的綠色的閃光光罩。

這似乎是不應該的。

當app執行的時候,只改變元件一小點地方,不應該導致整個元件的的重新渲染。

【譯】精挑細選的一份React效能問題優化的清單
實際更新應該發生在App元件的一小部分

更理想的高亮更新顯示應該是這個樣子:

【譯】精挑細選的一份React效能問題優化的清單
注意現在的高亮更新只顯示包含了一小塊更新的區域

在更復雜的應用中,無效浪費渲染的影響是巨大的!解決了重新渲染問題足夠提升應用的效能了。

看完這個問題,對此你有什麼解決辦法?

2. 將需要頻繁更新的區域抽離成獨立的元件

一旦你注意到了你應用中無用的渲染,從你的元件樹中拆分出頻繁更新的區域是一個不錯的開始。

下面具體說明如何拆分。

在 Cardie中,App元件通過react-reduxconnect函式連線到了redux store。從store中,它接受的props有:namelocationlikes以及description

【譯】精挑細選的一份React效能問題優化的清單
<App/>需要直接從redux store中接受的props

description的props定義了當前使用者的資訊。

本質上,無論何時點選按鈕改變了使用者資訊,description的值都會改變。它的改變導致了整個App元件的重新渲染。

你是否想起了React官網中說的,每當元件的 propsstate改變時,都會觸發該元件重新渲染。

【譯】精挑細選的一份React效能問題優化的清單

【譯】精挑細選的一份React效能問題優化的清單

【譯】精挑細選的一份React效能問題優化的清單
一個react元件會渲染一個由 propsstate元素定義的元件樹。如果propsstate改變,這個狀態元件樹就會重新生成一個新的樹

此時我們要把需要更新的元件單獨拆分出來渲染,而不是整個App元件毫無意義的重新渲染。

例如,我們可以建立一個叫Profession元件渲染自己的DOM元素。

【譯】精挑細選的一份React效能問題優化的清單

這樣的話,Profession元件就能渲染使用者資訊的description 的資料,列如I am a Coder

【譯】精挑細選的一份React效能問題優化的清單
<Profession/><App/>中渲染

此時元件樹看起來是這樣:

【譯】精挑細選的一份React效能問題優化的清單

對於profession的props,我們關注的重點不再是<App/>,而是變成了<Profession/>

【譯】精挑細選的一份React效能問題優化的清單
profession資料由<Profession/>元件直接從redux store獲取

不管你是否使用redux,這時改變profession的props,將不再導致App的重新渲染,而<Profession/>將重新渲染。

完成這個重構之後,你將得到理想的高亮更新顯示:

【譯】精挑細選的一份React效能問題優化的清單
想在高亮更新僅包含<Profession />

為了檢視程式碼的更改,請從遠端倉庫克隆切換分支到 isolated-component branch檢視

3. 適當地使用純元件

任何提到React效能的地方都會提及到pure components

然而,你知道怎樣在合適的時候使用純元件嗎?

當然,我們可以把每個元件都變成純元件,但請記住不要對外層的容器元件這樣操作。因為還有shouldComponentUpdate函式。

純元件沒有自己的state,因此,純元件的重新渲染僅僅由props改變導致的。

一個簡單的實現純元件的方法是用React.PureComponent代替React.Component

【譯】精挑細選的一份React效能問題優化的清單
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個子元件。

【譯】精挑細選的一份React效能問題優化的清單
注意Description元件有profession的prop,這個prop只傳遞給了Profession,其它3個元件並不關心profession的值。

當然這些新元件的內容是極其簡單的。例如,<I />元件僅僅渲染了一個span >I </span>元素。

有趣的是每當description的prop改變之後,Profession的每個子元件都會重新渲染。

【譯】精挑細選的一份React效能問題優化的清單
每當Description接受一個新的prop值,它的所有子元件都會重新渲染

我在每個子元件的render方法新增了logs日誌,你能夠確實看到每個子元件都被重新渲染了。

【譯】精挑細選的一份React效能問題優化的清單

你可以用 react dev tools檢視高亮更新部分

【譯】精挑細選的一份React效能問題優化的清單

這個行為是符合預期的。每當元件的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/>這些元件也不會重新渲染!

【譯】精挑細選的一份React效能問題優化的清單

在重構之後觀察高亮更新的顯示,你會發現<I/>, <Am/> 以及 <A/>這些元件都沒有更新,僅僅Profession元件改變了,因為它的prop改變了。

【譯】精挑細選的一份React效能問題優化的清單
<I/>, <Am/> 以及 <A/>元件沒有顯示高亮更新

在大型專案中,把某些元件改成純元件你會發現巨大的效能提升。

為了檢視程式碼的更改,請從遠端倉庫克隆切換分支到 pure-component branch檢視

4. 避免通過一個新的物件作為props

重複一遍,每當一個元件的props改變時,就會重新渲染這個元件。

【譯】精挑細選的一份React效能問題優化的清單

當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

【譯】精挑細選的一份React效能問題優化的清單
點選按鈕,<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效能問題優化的清單
在開發模式下執行構建,點選react開發者工具就會彈出警告

如果你使用了create-react-app的腳手架,執行生產模式打包,僅需執行命令: npm run build。 它將會盡可能優化壓縮你的程式碼。

6. 使用程式碼分割(code splitting)

當你打包你的應用,你可能會把整個專案打包成一個大的塊。

此時的問題,當你的應用變大的時候,那個塊也會變大。

【譯】精挑細選的一份React效能問題優化的清單
當使用者訪問網站,他就會獲取到整個專案的一個大塊

程式碼分割提倡的不是使用者立即就獲取到專案的整個塊,而是使用者需要的時候,才動態的載入相關的專案塊。

一個常見的例子就是通過路由來程式碼分割。這個方法,程式碼將會根據路由劃分成不同的塊。

【譯】精挑細選的一份React效能問題優化的清單
/home路由被劃分成了一個小塊,/about路由也一樣

也還有其他程式碼分割的方法。比如,如果一個元件當前對使用者來說是不可見的,那它就可以延遲載入,當使用者需要的時候在渲染。

無論你選擇哪一種方法,重要的是做好權衡,不要降低了使用者體驗。

程式碼分割是極好的,它能提高你應用的效能。

我僅僅從概念上解釋了程式碼分割的知識。如果你想知道更多程式碼分割的知識,請檢視React的官方文件,很好的解釋了程式碼分割的知識。

結尾

現在你已經獲取到一份還算可以的追蹤修復效能問題的清單了。快來讓你的應用更快吧!

廣告略過

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!

相關文章