譯: Lazy loading (and preloading) components in React 16.6
React 16.6新增了一個新的特性: React.lazy(), 它可以讓程式碼分割(code splitting)更加容易。
接下來通過一個股票App Demo, 來學習如何使用React.lazy這個新特性並瞭解為什麼要使用它。
我們建立了一個股票Web App,App展示了一些股票的列表,點選其中的一個股票,它會展示出最近這隻股票的走勢圖。
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 />
所消耗的時間記錄。
展示StockTable
一共耗時2470ms(模擬Fast3G網路環境與4核普通CPU)
通過下圖,可以瞭解到在向瀏覽器傳輸壓縮後的125K檔案中都包含了什麼
如我們所預期,頁面載入了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()
, 我們在程式碼中做了兩處變更。
首先,將靜態引用元件的程式碼import StockChart from "./StockChart"
替換為呼叫React.lazy()
,在lazy()
傳入一個匿名函式作為引數,在函式中動態引入StockChart
元件。這樣在我們渲染這個元件前,瀏覽器將不會下載./StockChart.js
檔案和它的依賴。
如果React要渲染<StockChart />
元件時,元件依賴的程式碼還沒下載好,會怎樣呢? 這就是為什麼我們新增了<React.Suspense/>
。在程式碼未下載好前,它將會渲染fallback
props屬性傳入的值,當全部子節點依賴的程式碼都準備好後,才會去渲染子節點內容。
現在App
將會被打包成兩個檔案。
main.js
檔案只有36kb,包含<StockChart />
及其依賴的程式碼檔案89KB。
在優化後, 如下圖,瀏覽器展示了<StockTable />
需要消耗的時間。
瀏覽器用了760ms去下載main.js
(以前是1250ms)和執行指令碼消耗61ms(以前是487ms). 展示<StockTable />
只用了1546ms(以前是2460ms)。
預載入-懶載入元件
現在我們已經讓App載入的更快了。但還有另一個問題。
使用者在第一次點選Item時,會展示"Loading...."的回退方案的元件。這是因為App
需要等待瀏覽器載入好<StockChart />
的程式碼。
如果我們想避免展示"Loading...."這樣的loading狀態,我們需要在使用者點選之前就載入好程式碼。
一個簡單實現預載入程式碼的方式就是提前呼叫React.lazy()
const stockChartPromise = import("./StockChart");
const StockChart = React.lazy(() => stockChartPromise);
複製程式碼
當我們呼叫dynamic imoprt
時,元件就會開始載入,並且它不會阻塞<StockTable />
元件的載入。
看下App載入的記錄以及與未修改版本的對比
當使用者在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>
);
}
}
複製程式碼
在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社群,每週定時推送優質文章。
- 週刊文章集合: weekly