基於React.Suspense和React.lazy的前端性能優化


文章放在庫存很久了。。。

React16.6於2018年10月份發佈,該版本帶來了許多新的特性同時賦予給React更強大的功能。其中最爲顯著的兩個特性是 React.SuspenseReact.lazy。這兩個特性,將React的代碼分割和懶加載帶到了一個新的高度。使用這兩個特性,你能夠作到的是在真正須要時才加載該組件的文件。前端

本文主要介紹我在項目中如何使用 React.SuspenseReact.lazy以及該特性給咱們React開發者帶來的好處。react

1、爲何要使用代碼分割

隨着前端技術的不斷髮展,ES6模塊、Babel轉換、webpack打包等新技術的出現,前端應用如今徹底可使用模塊化的方式完成,便於維護。 一般狀況下,咱們會將全部的模塊打包到一個文件中,當請求網頁時加載該文件以展現整個應用。可是,隨着網頁功能的不斷擴展,這便帶來了網頁加載緩慢、交互卡頓等問題,使用戶體驗很是糟糕。 致使這一問題的主要緣由是,咱們在頁面加載時,會一次性加載全部代碼,不管是當前要用的代碼仍是以後用到的代碼。可是用戶在第一次進來時並不會用到全部的功能,所以 code-splitting即代碼分割這個名詞出現了。webpack

像webpack便提供了代碼分割的功能。webpack中對代碼分割的定義以下:git

Code splitting is one of the most compelling features of webpack. This feature allows you to split your code into various bundles which can then be loaded on demand or in parallel.github

意思就是說能夠將代碼拆分爲多個bundle,同時能夠按需或者並行加載。所以,爲了提升應用的性能,咱們能夠嘗試如何合理的分割代碼並延遲加載。web

2、如何分割代碼?

一、動態加載

ES6標準引入了import以方便咱們靜態加載模塊。形式如:瀏覽器

import xxx from xxx.js複製代碼

儘管import對於咱們加載模塊頗有幫助,可是靜態加載模塊的方式必定程度上限制了咱們來實現異步模塊加載。不過,目前動態加載模塊的import()語法已處於提案階段,而且webpack已將他引入並使用。import()提供了基於Promise的API,所以,import()的返回值是一個完成狀態或拒絕狀態的Promise對象。形式如:bash

import(/* webpackChunkName: 'module'*/ "module")
    .then(() => {
        //todo
    })
    .catch(_ => console.log('It is an error'))
複製代碼

webpack在編譯時,識別到動態加載的import語法,則webpack會爲當前動態加載的模塊建立一個單獨的bundle。若是你使用的是官方的Create-react-app腳手架或React的服務端渲染框架Next.js,那麼能夠直接使用動態import語法。若是你的腳手架是你本身配置的webpack,那麼你須要按照官方指南來設置,請移步[1]。網絡

二、動態加載React組件

當前最爲流行的一種方法是使用 React-loadable[2]庫提供的懶加載React組件。它利用import()語法,使用Promise語法加載React組件。同時,React-loadable支持React的服務端渲染。 一般,咱們以以下方式實現組件:app

import LazyComponet from 'LazyComponent';

export default function DemoComponent() {
    return (
        <div>
            <p>demo component</p>
            <AComponent />
        </div>
    )
}
複製代碼

在上面的例子中,假設 LazyComponetDemoComponent渲染時咱們並不展現。可是由於咱們使用import語法將 LazyComponet導入,因此在編譯時會將 LazyComponet的代碼與 DemoComponent的代碼打包到同一個bundle裏面。 可是,這並非咱們想要的。因此咱們能夠經過使用 React-loadable來懶加載 LazyComponet,同時將 LazyComponet的代碼單獨打包到一個bundle裏面。咱們能夠看一下官網提供的例子:

import Loadable from 'react-loadable';
import Loading from './my-loading-component';

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  loading: Loading,
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}
複製代碼

從例子中咱們能夠看到,react-loadable使用動態import()方法,並將導入的組件分配給loader屬性。同時,react-loadable提供了一個loading屬性,以設置在加載組件時將展現的組件。

3、React.lazy和React.suspense的使用

React.lazy and Suspense is not yet available for server-side rendering. If you want to do code-splitting in a server rendered app, we recommend Loadable Components. It has a nice guide for bundle splitting with server-side rendering.

在使用以前,咱們須要特別注意的一點是,React官方明確支持,React.lazy和Suspense並不支持服務端渲染。所以,使用服務端渲染的同窗,請繞行至 react-loadableloadable-components[3]。

因爲我是對原有項目進行的升級,所以,本文如下內容主要針對於老項目升級React最新版所作的工做。

一、代碼升級React最新版

若是你的代碼是Reactv16,那麼能夠直接升級到最新版本,固然React16.6已經提供了lazy和suspense方法。若是是v16以前,則按照官方操做遷移。

二、肯定原有代碼的懶加載組件

首先,按照需求,將非首屏加載的組件肯定爲懶加載組件,個人項目共肯定五個組件能夠進行懶加載。修改方式很簡單,原有引入組件的方法爲:

import LazyComponent from "../components/LazyComponent ";複製代碼

修改成:

const LazyComponent = React.lazy(() =>
  import(/* webpackChunkName: 'lazyComponent'*/ "../components/LazyComponent")
);
複製代碼

如代碼所示:將靜態引用組件的代碼替換爲調用React.lazy(),在lazy()傳入一個匿名函數做爲參數,在函數中動態引入 lazyComponent組件。這樣在咱們渲染這個組件前,瀏覽器將不會下載 lazyComponent.bundle.js文件和它的依賴。 其中,import內的webpackChunkName爲咱們定義的bundle文件名。

若是React要渲染組件時,組件依賴的代碼還沒下載好,會出現什麼狀況? <React.Suspense/>的出現幫咱們解決問題。在代碼未下載好前,它將會渲染fallback屬性傳入的值。所以咱們的原有代碼爲:

return (<div><MainComponet /><LazyComponent /></div>)複製代碼

修改成:

return (
        <div>
            <MainComponet />
            <React.Suspense fallback={<div>正在加載中</div>}>
                <LazyComponent />
            </React.Suspense>
        </div>
    )
複製代碼

fallback中能夠修改成任意的spinner,本次不作過多優化。假如你不使用React.suspense,則React會給出你錯誤提示,所以記得React.lazy和React.Suspense搭配使用。 此時咱們能夠看一下咱們的網絡請求。

enter image description here

從圖中咱們能夠看到,咱們動態加載的lazyComponet組件被單獨打包到一個bundle裏面,然而,在首屏加載的時候,該bundle已經加載到咱們的頁面中了,這也許並非咱們想要的,咱們想要的是當咱們須要的時候再加載。接下來咱們就控制一下,當咱們須要的時候,再加載該文件。

三、經過變量控制加載

本來我選擇的五個懶加載組件均屬於彈層性質的組件,所以必然會設置一個state來控制該組件的顯示與隱藏,所以咱們將代碼改成:

return (
        <div>
            <MainComponet />
            {this.state.showLazyComponent && (
                <React.Suspense fallback={<div>正在加載中</div>}>
                    <LazyComponent />
                </React.Suspense>
            )}

        </div>
    )
複製代碼

此時咱們能夠看一下網絡請求。

enter image description here

從圖中能夠看出,在首屏加載時,並未加載咱們的懶加載組件 LazyComponent所對應的bundle包。等到咱們點擊須要該組件顯示時,頁面纔去加載該js。這便達到了咱們代碼分離並懶加載的目的。那麼咱們這麼操做,到底主bundle包的體積減小了嗎?接下來咱們打包文件看一下。

四、打包文件

拆分前打包出來的文件:

Alt text

拆分後打包出來的文件:

Alt text

app.js文件變小,隨之增長 lazyComponent.js。當懶加載組件多時,咱們即可必定程度上減小首屏加載文件的大小,提升首屏的渲染速度。

4、驗證優化的有效性

一、利用Puppeteer和Performance API作對比

爲了驗證前面我所作的優化的有效性,我作了一組對比實驗。實驗內容爲使用puppeteer分別訪問優化前和優化後的頁面1000次,使用Performance API分別統計五項數據在這1000次訪問時的平均值。 實驗結果以下圖所示,其中:

  • A爲request請求平均耗時

  • B爲解析dom樹耗時平均耗時

  • C爲請求完畢至DOM加載平均耗時

  • D爲請求開始到domContentLoadedEvent結束平均耗時

  • E爲請求開始至load平均耗時 Alt text

折線圖沒法準確展現數據,所以,附表格數據以下:

類別 優化後 優化前
A(request請求平均耗時) 7.013 7.042
B(解析dom樹耗時平均耗時) 30.284 32.597
C(請求完畢至DOM加載平均耗時) 552.861 582.01
D(請求開始到domContentLoadedEvent結束平均耗時) 569.137 589.07
E(請求開始至load平均耗時) 1055.597 1126.941

從數據中咱們能夠看出,優化先後請求時間並無什麼影響,可是整體load的時間明顯縮短並立刻進入1000ms大關,可見優化後對於首屏的加載速度仍是有明顯提高。

注:因puppeteer運行1000次的過程當中,會出現網絡波動的狀況,致使有些請求的數據偏大,所以平均值並不能徹底體現正常的請求時間。但1000次的平均值足以進行優化先後的請求時間對比。

二、利用Chorme Performance 參數作對比

由於Performance API提供的參數有限,所以我從Chrome瀏覽器的performance summary中拿到了單次頁面請求時的參數。由於是單次數據,所以咱們不進行詳細的對比。在此列出,只爲說明優化先後瀏覽器渲染時間上哪些部分有提高。

 優化前:

Alt text

優化後:

Alt text

  • 藍色:加載(Loading)時間下降

  • 黃色:腳本運算(Scripting)時間下降

  • 紫色:渲染(Rendering)時間下降

  • 綠色:繪製(Painting)時間持平

  • 灰色:其餘(Other)時間下降

  • 閒置:瀏覽器空閒時間下降

另外,我從Network中發現,優化後由於頁面解析的相對以前較快,所以主接口的請求時間也相應的提早了一些。

5、總結

從多項數據代表, React.lazyReact.Suspense的使用必定程度上加快了首屏的渲染速度,使得咱們的頁面加載更快。 另外,當咱們想添加一個新功能而引入一個新依賴時,咱們每每會評估該依賴的大小以及引入該依賴會對原有bundle形成多大影響。假如該功能不多被用到,那麼咱們能夠痛快地使用 React.lazyReact.Suspense來按需加載該功能,而無需犧牲用戶體驗了。

6、擴展閱讀

[1] https://webpack.js.org/guides/code-splitting/ 

[2] https://github.com/jamiebuilds/react-loadable 

[3] https://github.com/smooth-code/loadable-components

相關文章
相關標籤/搜索