漸進式React

能夠說 React 爲Web開發者帶來了全新的開發模式,而在各種新功能下,如何達到性能最優還是咱們須要關心的。今天作一次精讀嘗試,原文地址在文末,話很少說,先呈上一份性能清單:javascript

1. 測量組件級渲染性能css

  • Chrome DevTools Performance 面板
  • React DevTools profiler 面板

2. 避免非必要的組件重複渲染html

  • 儘可能使用shouldComponentUpdate
  • Class 組件使用PureComponent
  • 功能組件使用React.memo
  • 記住 Redux selectors(好比使用reselect
  • 虛擬化超長列表(好比使用react-window

3. 使用 Lighthouse 測量App級性能
4. 提高APP級性能java

  • 若是沒有使用服務端渲染,則使用React.lazy分割組件
  • 若是使用了服務端渲染,則使用loadable-components之類的庫來分割組件
  • 使用 service worker 來緩存須要的文件,Workbox 能夠幫到你
  • 若是使用了服務端渲染,使用流式傳輸(使用renderToNodeStreamrenderToStaticNodeStream
  • 沒法使用 SSR?使用react-snap等方案進行預渲染(Pre-render)
  • 若是用到 CSS-in-JS 庫,將關鍵路徑樣式解析出來
  • 保障應用可用性,考慮使用React A11yreact-axe等庫
  • 若是用戶須要經過設備主屏幕訪問站點,增長 web app manifest

對於 React 應用,咱們主要關注兩個性能維度:組件渲染性能 與 頁面加載性能,因爲 React 的核心在於組件設計,那先從組件性能講起。react

測量組件級性能

React 熟爲人知的「Virtual DOM」,是創建在高效調和(reconciliation)算法基礎上的,其基於必定約定假設,將虛擬 DOM Diff 時間複雜度從O(n3)降爲O(n)。雖然這些 React 內部實現不要求你們都理解,在小型應用中性能也不足以成爲瓶頸,但性能優化原本就是量變到質變的過程,所以讓咱們從測量組件性能工具作起。webpack

使用 Chrome 開發者工具測量性能

React 使用 User Timing API 收集各生命週期耗時,爲避免測量自己帶來的性能影響,性能採集僅在開發模式有效。
圖片描述
說實話,這類火焰圖在視覺上有很強直觀性,但缺乏的有效調試信息,所以 React Devtools 提供了更爲強大的能力。git

使用 React DevTools Profiler 分析性能

React 16.5 開始使用 Profiler API 收集組件渲染耗時,以獨立Tab形式呈如今 React DevTools 中。它的使用相似於 Chrome DevTools Performance,經過錄制來決定收集數據範圍。github

React DevTools Profiler 示例

相比 Chrome DevTools Performance 中呈現的 Timing 信息,React DevTools Profiler 提供了更多輔助定位性能瓶頸的組件級信息,這裏簡單說下幾個亮點:web

  1. 以 commit 維度記錄信息。熟悉 React 內部原理的同窗知道,React 生命週期中有個 Commit 階段,React DevTools Profiler 會以每次 commit 維度記錄渲染相關信息,在右側進行展現。
  2. 具體組件狀態信息。左側的火焰圖對應了組件層級結構,以不一樣顏色區分組件渲染次數,高亮重複渲染的組件。點擊組件後,右側會展現組件具體渲染次數,以及當時的 state 與 props。
  3. 簡單的統計能力。除了火焰圖,工具還有排名(Ranked)和交互(Interactions)兩個維度統計,幫助更快的定位組件瓶頸。

整體上 Profiler 工具使用簡單,沒什麼門檻,接下來介紹優化組件渲染的相關技術。算法

避免非必要的組件重複渲染

去除無用的重複渲染,方案因場景各異:
使用 shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState) {
  // 僅在肯定條件下返回 true
}

Class 組件使用 PureComponent

import React, { PureComponent } from 'react';

class AvatarComponent extends PureComponent {

}

功能組件使用 memo

import React, { memo } from 'react';

const AvatarComponent = memo(props => {

});

記住 Redux selectors(好比使用 reselect
虛擬化超長列表(好比使用 react-window

測量 App 級性能

除了 DOM 級的渲染性能,還有更高層面的應用加載性能須要關注。這方面的性能工具屬 Lighthouse 最有名了,咱們能夠經過 Node CLI、Chrome 擴展和 Chrome DevTools 的 Audits 面板用到它。
圖片描述
Lighthouse 根據一系列性能規則,對目標頁面進行檢查,最終生成一份性能報告,給出未達標指標的改進建議。在 React 項目中,隨着路由和組件的膨脹,很容易觸發 Lighthouse 對 JavaScript 傳輸體積的檢查規則(Avoid enormous network payloads)。在實踐中,已有成熟的方案供咱們使用——代碼分割。

代碼分割

進行代碼分割的一個方法是動態導入(dynamic imports)

import('lodash.sortby')
    .then(module => module.default)
    .then(module => doSomethingCool(module))

這裏的 import 語法像是函數調用,容許異步加載模塊並經過 Promise 返回。上面代碼動態獲取了 lodash sortby 方法,緊接着被後續代碼使用。

雖然動態導入目前仍處於 stage 3 階段,Chrome and Safari 已經率先支持了,WebpackRollupParcel 也作好了支持。
回到 React,組件級別的代碼分割已經被良好地抽象,好比React.lazy

import React, { lazy } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

然而這麼作可能會致使用戶可感知的加載延遲。對此,能夠將Suspense組件配合React.lazy一塊兒使用,「暫停」部分組件的渲染,經過渲染 Loading 組件,對仍在加載的組件進行降級處理:

import React, { lazy, Suspense } from 'react';
import LoadingComponent from './LoadingComponent';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

const PageComponent = () => (
  <Suspense fallback={LoadingComponent}>
    <AvatarComponent />
  </Suspense>
)

Suspense 還不支持 SSR,若是要在服務端渲染使用代碼分割,可使用loadable-components這樣的庫。另外若是須要在滾動場景作異步加載的同窗,能夠了解下 react-loadable-visibility

緩存

Service Worker 就不從新介紹了,歸納起來就是一個運行在瀏覽器後臺的可編程代理,讓咱們對網絡緩存更加可控。一個具體的使用場景是,經過控制緩存策略,來提高用戶二次訪問時的頁面加載體驗。

這裏主要是安利 Workbox 這個工具包,它能讓咱們更簡單地使用 Service Worker,具體細節不作展開,在 PWA 的浪潮中,你的站點值得擁有。

流式 SSR

爲了加快頁面呈現,服務端渲染概念已經被你們接受和使用。爲了最大限度複用服務端返回的 HTML,React 還提供了 hydrate() API。這時優化的目光投向了 TTI,流式渲染也應運而生,相對以前的renderToString API 返回 HTML 字符串,renderToNodeStream會返回 NodeReadable字節流。這樣瀏覽器就能源源不斷地獲取到頁面塊,hydrate API 也很好地支持了流式處理,真的很強大。

關於 SSR 更多信息,能夠查看本專欄的《Web渲染那些事兒》

SSR 不行?預渲染來頂

其實服務端渲染是個籠統的概念,因爲現代頁面大多都是動態的,所以每一個請求可能都要在服務器上處理一遍。然而純服務端渲染與純客戶端渲染之間,是存在中間地帶的。雖然頁面是經過組件模式進行開發,但頁面內容多是靜態的,只要生成一次就行,這就是預渲染(Prerendering)或靜態渲染的由來。

這裏介紹一個基於 Puppeteer 的預渲染方案 react-snap,它能讓你更簡單地進行預渲染頁面。

提取關鍵 CSS-in-JS 樣式

出於各類緣由,有些開發者會使用 emotionstyled-components 等 CSS-in-JS 庫,但若是不注意,會致使樣式都在運行時解析,也就是致使頁面會閃過無樣式的瞬間。若是在移動設備或弱網絡場景下,體驗就很糟糕。上面提到的 SSR 更是如此,由於在客戶端JS加載以前,SSR 返回的無樣式 DOM 已經開始渲染了。
圖片描述
優化的作法就是將這些關鍵樣式提取出來,好在 emotion 和 styled-components 都原生支持將樣式提取到可讀流中,流式 SSR 也不用擔憂閃屏狀況了。

雜項

接下幾項關於提高開發者體驗,並助於減小繁瑣的編碼。

編寫更少代碼 = 傳輸更少代碼 = 更快的網頁加載

原子 CSS

原子樣式的理念是定義單一做用的 class,以達到靈活組合樣式的目的。看個簡單的例子:

<button class="bg-blue">Click Me</button>

bg-blue 定義了藍色背景色,做用在 button 上可令其應用這條規則。若是要給它加個 padding,能夠設置單獨負責 padding 的 class:

<button class="bg-blue pa2">Click Me</button>

雖然會多寫幾個 css class,但能夠不用再去編輯複雜的 CSS 文件了,若是你不想本身維護一套樣式規範,能夠直接用開源的 Tachyons 方案。組件級別還有 tachyons-components 這樣的方案,我的以爲還不太成熟,這裏不作展開。

總體來看原子 CSS 比較適用於樣式風格簡單統一的場景,讓開發者聚焦 JS 部分,隨時修改樣式而不用關心樣式繼承方面的影響,另外一個好處是 CSS 能夠長期緩存,基本不須要更新。

出於性能考慮,頁面首次加載會被統同樣式的 CSS 阻塞,看了下gzip後有10KB大小,仍是看場景應用吧。

Hooks

Hooks 容許以功能組件實現之前只有class組件才能實現的功能,好比對state的操做:

import { useState } from 'react';

function AvatarComponent() {
  const [name, setName] = useState('Houssein');

  return (
    <React.Fragment>
      <div>
        <p>This is a picture of {name}</p>
        <img src="avatar.png" />
      </div>

      <button onClick={() => setName('a banana')}>
        Fix name
      </button>
    </React.Fragment>
  );
}

除了 React 提供的 useStateuseEffect,能夠自定義 hooks 來複用跨組件的邏輯。在此以前要實現該功能,會用到 recompose 這個庫,Hooks 出現後就能夠退出歷史舞臺了。(真實狀況是 recompose 的做者加入了 React Team,並推出了 Hooks)

雖然 Hooks 的定位是解決代碼架構問題,但確實也在加載性能方面作出了貢獻。雖然 Hooks 功能相關代碼爲 React 增長了1.5KB(gzip後),但 Hooks 代碼比 class 組件代碼更易壓縮,所以能夠減少一些 JS 包大小。

總結

像 React 這樣擁有普遍開發者的開源項目,有兩樣事能夠期待:

  1. 優化其 API,令構建應用更加容易
  2. 開源社區貢獻第三方庫,令構建應用更加容易

「令構建應用更加容易」能夠指不少方面,讓開發者作的更少、頁面性能更高是其中之一。

延伸閱讀

progressive react
React as a UI Runtime
Debugging React performance with React 16 and Chrome Devtools
Introducing the React Profiler
make loadable-components work with SSR
《Web渲染那些事兒》

相關文章
相關標籤/搜索