【譯】精挑細選的一份 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/><Profession/>元件都重新渲染了。事實上,props 並沒有發生改變,為什麼重新渲染了呢?

為什麼?
一旦 Description 元件接受了一個新的 props,就會呼叫 render 生成一個 React 的元件樹。

在呼叫 render 函式之後,它就會建立一個新的 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 的官方文件,很好的解釋了程式碼分割的知識。

結尾

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

廣告略過

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

相關文章