[譯] React 16.6 懶載入(與預載入)元件

騰訊IVWEB團隊發表於2019-01-06

譯: Lazy loading (and preloading) components in React 16.6

React 16.6新增了一個新的特性: React.lazy(), 它可以讓程式碼分割(code splitting)更加容易。

接下來通過一個股票App Demo, 來學習如何使用React.lazy這個新特性並瞭解為什麼要使用它。

我們建立了一個股票Web App,App展示了一些股票的列表,點選其中的一個股票,它會展示出最近這隻股票的走勢圖。

[譯] React 16.6 懶載入(與預載入)元件
try it

以上就是App的全部功能了。 你可以在Github Repo閱讀專案原始碼(也可以通過PR,檢視每一次提交的專案變更和可執行版本。)

在本文,我們只關心App.js這個檔案內的程式碼邏輯。

import React from "react";
import StockTable from "./StockTable";

import StockChart from "./StockChart";

class App extends React.Component {
  state = {
    selectedStock: null
  };
  render() {
    const { stocks } = this.props;
    const { selectedStock } = this.state;
    return (
      <React.Fragment>
        <StockTable
          stocks={stocks}
          onSelect={selectedStock => this.setState({ selectedStock })}
        />
        {selectedStock && (
          <StockChart
            stock={selectedStock}
            onClose={() => this.setState({ selectedStock: false })}
          />
        )}
      </React.Fragment>
    );
  }
}

export default App;
複製程式碼

App元件獲取股票列表的資料並且展示了<StockTable/>元件。當其中一個股票被點選選中時, App將會展示那隻股票的走勢圖<StockChart>

這裡有什麼問題呢?

我們想要App儘可能快速載入與展示<StockTable />, 但App卻要等待瀏覽器下載(解壓, 分析, 編譯, 執行等)StockChart的程式碼。

通過Chrome DevTools可以看到展示<StockTable />所消耗的時間記錄。

Trace without lazy loading

展示StockTable一共耗時2470ms(模擬Fast3G網路環境與4核普通CPU)

通過下圖,可以瞭解到在向瀏覽器傳輸壓縮後的125K檔案中都包含了什麼

Webpack Budle Analyzer Report

如我們所預期,頁面載入了react, react-dom 和一些react的依賴包,但頁面也同時載入了元件依賴的moment, lodash, victory的依賴。展示<StockTable />是不需要這些依賴的。

那如何載入 <StockChart />的依賴才不會影響的載入速度呢?

懶載入元件

通過使用webpack的'dynamic import', 我們可以將打包的程式碼拆分成兩部分,main檔案裡包含了需要展示<StockTable>的程式碼及依賴。另一個檔案包含了展示<StockChart />的程式碼及依賴包。

dynamic import技術是十分有用的,所以React16.6版本新新增了一個API - React.lazy(), 可以更便利地去非同步引用React元件。

為了在App.js中使用React.lazy(), 我們在程式碼中做了兩處變更。

[譯] React 16.6 懶載入(與預載入)元件

Diff

首先,將靜態引用元件的程式碼import StockChart from "./StockChart"替換為呼叫React.lazy(),在lazy()傳入一個匿名函式作為引數,在函式中動態引入StockChart元件。這樣在我們渲染這個元件前,瀏覽器將不會下載./StockChart.js檔案和它的依賴。

如果React要渲染<StockChart />元件時,元件依賴的程式碼還沒下載好,會怎樣呢? 這就是為什麼我們新增了<React.Suspense/>。在程式碼未下載好前,它將會渲染fallbackprops屬性傳入的值,當全部子節點依賴的程式碼都準備好後,才會去渲染子節點內容。

現在App將會被打包成兩個檔案。

[譯] React 16.6 懶載入(與預載入)元件

main.js檔案只有36kb,包含<StockChart />及其依賴的程式碼檔案89KB。

在優化後, 如下圖,瀏覽器展示了<StockTable />需要消耗的時間。

瀏覽器用了760ms去下載main.js(以前是1250ms)和執行指令碼消耗61ms(以前是487ms). 展示<StockTable />只用了1546ms(以前是2460ms)。

預載入-懶載入元件

現在我們已經讓App載入的更快了。但還有另一個問題。

[譯] React 16.6 懶載入(與預載入)元件

使用者在第一次點選Item時,會展示"Loading...."的回退方案的元件。這是因為App需要等待瀏覽器載入好<StockChart />的程式碼。

如果我們想避免展示"Loading...."這樣的loading狀態,我們需要在使用者點選之前就載入好程式碼。

一個簡單實現預載入程式碼的方式就是提前呼叫React.lazy()

const stockChartPromise = import("./StockChart");
const StockChart = React.lazy(() => stockChartPromise);
複製程式碼

當我們呼叫dynamic imoprt時,元件就會開始載入,並且它不會阻塞<StockTable />元件的載入。

看下App載入的記錄以及與未修改版本的對比

[譯] React 16.6 懶載入(與預載入)元件

當使用者在1s內點選Item時,才會看到“Loading...”

你也可以使用你自己的方式去優化lazy函式,讓預載入元件更加通用方便。

function lazyWithPreload(factory) {
  const Component = React.lazy(factory);
  Component.preload = factory;
  return Component;
}

const StockChart = lazyWithPreload(() => import("./StockChart"));

// somewhere in your component 
...
  handleYouMayNeedToRenderStockChartSoonEvent() {
    StockChart.preload();
  }
...
複製程式碼

預渲染元件

以上功能已經滿足Demo App的使用了。但對於更大型的專案,在懶載入元件被載入之前,元件可能還會有其他懶載入元件的程式碼或資料,所以使用者還是需要時間等待元件載入。

那另外一種預載入元件的方式就是提前渲染它。在頁面中渲染元件,但是並不在頁面中展示,也就是隱藏渲染。

class App extends React.Component {
  state = {
    selectedStock: null
  };
  render() {
    const { stocks } = this.props;
    const { selectedStock } = this.state;
    return (
      <React.Suspense fallback={<div>Loading...</div>}>
        <StockTable
          stocks={stocks}
          onSelect={selectedStock => this.setState({ selectedStock })}
        />
        {selectedStock && (
          <StockChart
            stock={selectedStock}
            onClose={() => this.setState({ selectedStock: false })}
          />
        )}
        {/* Preload <StockChart/> */}
        <React.Suspense fallback={null}>
          <div hidden={true}>
            <StockChart stock={stocks[0]} />
          </div>
        </React.Suspense>
      </React.Suspense>
    );
  }
}
複製程式碼

diff

在App第一次渲染後,React將會載入<Stockchart />並嘗試去渲染元件,所以元件需要的依賴或程式碼也會被載入。

我們將懶載入元件包裹在一個隱藏的div中, 在載入之後頁面不會展示任何東西。並且還用了React.suspense包裹住這個div,且其fallback值為null,這樣它在載入時也不會被展示出來。

注:hidden屬性通常表明該節點是不相關的,瀏覽器將不會渲染具有這個屬性的元素。而React並不會對這個屬性做任何特殊處理(但在未來的版本中可能會較低優先順序處理被隱藏的元件)

還有什麼呢?

最後一個方法在很多場景下是可用的,但它仍有一些問題。

第一, 對於被隱藏渲染的懶載入元件, hidden屬性並不是完全有效的。舉個例子,使用portal的懶載入元件將不會被隱藏(可以使用portal來實現隱藏功能且不用一個額外的div,但這只是一個hack方式,在未來它將不會被使用)。

第二,雖然Dom節點已經被隱藏,但還是新增了額外的Dom節點,這可能會成為一個效能問題。

一個更好的辦法就是,通知react去渲染這個懶載入元件,但在載入後並不把它新增到Dom樹中。但據我所瞭解,在當前版本的React中是無法實現的。

另外我們能做的改進就是重複使用我們預渲染時準備的Dom,這樣當真正要去渲染圖表元件時,React就無需再建立一遍它。如果使用者將會點選某隻股票,我們可以在使用者點選前用正確的資料渲染它(就像這樣)

這就是文章的全部了,感謝閱讀。


《IVWEB 技術週刊》 震撼上線了,關注公眾號:IVWEB社群,每週定時推送優質文章。

相關文章