1. 程式人生 > >一個用於解決 React 常見問題的 Checklist

一個用於解決 React 常見問題的 Checklist

這是一份非常實用的,一步步解決 React 效能問題的清單。在這裡插入圖片描述

你想不想讓你的 React 應用程式執行更快?

想不想有一份清單來檢查常見的 React 效能問題?

如果你的答案,都是 yes~那就抓緊時間來讀一下這篇文章。這是一份非常實用的解決 React 效能問題的清單

在本文中,我將為大家介紹一個逐步消除常見效能問題的指南。

我會先描述問題,然後再給出解決方案。在這同時,你可以將同樣的問題帶入到你自己的專案中審查。

下面就開始動手吧~

專案

為了大家都方便學習,我會在同一個 React 應用上來介紹各種問題的情況。 專案名稱為 Cardie 可以在 Github 上下載Cardie 原始碼,跟著我一起學習。

The Cardie App.

Cardie 是一個功能非常簡單的專案。就是常見的個人簡介頁,用來展示一個使用者的個人介紹。 The User Details Being Displayed

頁面內包含一個可點選的按鈕,可以操作更改使用者的職業資訊。

在這裡插入圖片描述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, likesdescription

在這裡插入圖片描述<App/> 直接操作 redux store 獲取資料

在個人介紹頁目前定義了 description 屬性。 原因就是因為,點選按鈕的時候, description 屬性發生了改變。改變引發了 App 元件的重新渲染。

記不記得 React 101 裡有句話,一個元件中無論是 props 還是 state 發生改變,都會觸發重新渲染。

在這裡插入圖片描述

React 元件的元素渲染樹。這些元素是通過 propsstate 定義的。如果 props 或者 state 發生改變,元素樹會重新渲染。結果就會是一個新的樹。

我們應該如何讓一個特定的 React 元件元素更新,而不是 App 元件?

例如: 我們可以建立一個新元件,名字叫 Profession。渲染它自己的元素。

在這裡插入圖片描述

在這個例子裡,如果將職業點選切換為“我是一個程式設計師”的時候, Profession 元件會被重新渲染。 在這裡插入圖片描述在 元件內的 元件會重新渲染

新的元件樹如圖: 在這裡插入圖片描述

元件渲染了包括 元件的元素

也就是說之前是 元件在關心 profession 屬性,現在變成了 <Profession/> 這個元件的事情。 在這裡插入圖片描述

元件 <Profession/> 會直接從 redux store 獲取 profession 屬性。

不管你是否使用了 redux,這裡的關鍵點是 App 元件不再會因為 profession 屬性的改變,而引發重繪。而是被元件 <Profession/> 取代了。

更改之後,我們再來看一下 update highlighted 的表現:

在這裡插入圖片描述

注意只有 <Profession /> 內部發生了更新

3. 在恰當的地方使用靜態元件

React 提到效能問題,不得不說的就是靜態元件。那麼,怎樣正確使用靜態元件

當然,你可以把每個元件都寫成靜態元件,但是你要記得,有一個方法特殊。 shouldComponentUpdate 方法。

我們假定只有 props 和之前的 propsstate 不相同的時候,會引發元件的重新渲染。

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, ama. 都被重新渲染過

這是意料之中的事情。無論是 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 101React 會對新舊 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 體驗更快吧!