利用React 16.6新特性優化應用效能

Gavin1995發表於2019-03-04

利用React v16.6 Lazy&Suspense提升應用效能

本篇文章示例程式碼

github.com/Gavin1995/r…

前言

利用懶載入(Lazy Loading)優化頁面效能不是什麼新概念,不過React 16.6可以使用React.lazySuspense讓原生React實現Lazy Loading大大的簡化。

本篇文章也會擴充多種類似解決方案,見擴充部分

主要關鍵詞說明

動態匯入(Dynamic Import)

動態匯入目前只是TC39的一個提案,不是JS(ES)標準的一部分。該功能可以動態化載入我們分割的元件/頁面/檔案,具體提案見下方連結:
TC39 proposal-dynamic-import提案

目前要使用動態匯入,需要使用到babel外掛:

babel-plugin-syntax-dynamic-import

React.lazy

lazy函式提供了一種非常簡便的方法動態匯入元件,實現按需載入與程式碼分割

點選檢視文件

React.lazy使用方式非常簡單,示例:

// lazy 接受一個函式作為引數
const MyComponent = React.lazy(() => import(`path/MyComponent`));

// 展示時才載入(可以自定義一些條件)
const App = () => (
  <div>
    <MyComponent />
  </div>
)
複製程式碼

Suspense

Suspense元件用於包裝lazy元件,在lazy元件還沒有完全載入時,將fallback內容呈現給使用者。

用動態載入,編譯時會將檔案分割,從載入檔案到呈現會有時間延遲,此時可以使用Suspense展示一個loading。

點選檢視文件

使用示例

import React, { Suspense } from `react`;

const MyComponent = React.lazy(() => import(`path/MyComponent`));
const Loading = (
  <h3>loading...</h3>
);

const App = () => (
  <div>
    <Suspense fallback={Loading}>
      <MyComponent />
    </Suspense>
  </div>
)
複製程式碼

ErrorBoundary

ErrorBoundary(錯誤邊界),可以使用ErrorBoundary包裝Suspense,當Suspense出錯時,統一處理錯誤。當然ErrorBoundary不止可用於Suspense,可以包裝任意元件,然後統一處理錯誤。

實際場景中,可以自己先寫一些通用錯誤頁/元件繼承自ErrorBoundary,然後包裝要處理的元件。

也可以在元件的componentDidCatch生命週期中單獨處理錯誤。

點選檢視文件

應用

效能優化說明

針對React載入的優化,可以分為兩類:

  • 頁面載入優化
  • 元件載入優化

很明顯,React.lazy + Suspense可以很方便的應用於元件載入優化場景。

示例場景

CSR(客戶端渲染)應用多頁面的中某個耗時元件的載入

路由層

import React from `react`;
import { BrowserRouter as Router, Route } from `react-router-dom`;

import Header from `./header`;
import Index from `../pages`;
import Welcome from `../pages/welcome`;

export default () => (
  <Router>
    <div>
      <Header />

      <Route exact path="/" component={Index}/>
      <Route path="/welcome" component={Welcome}/>
    </div>
  </Router>
);
複製程式碼

某個存在耗時元件的頁面

// welcome.js
import React, { Suspense } from `react`;

// 使用Suspense + React.lazy動態載入的耗時元件
const Content = React.lazy(() => import(`../components/content`));

export default () => (
  <div>
    <h1>Welcome</h1>
    <Suspense fallback={<div>loading...</div>}>
      <Content/>
    </Suspense>
  </div>
);
複製程式碼

說明

假設用webpack編譯上面程式碼產出bundle.js。當進入首頁時,載入的bundle.js不會包含Welcome Content部分的程式碼,切換頁面到Welcome時會走網路載入需要的檔案。

利用這種方法拆分多個元件時,可以顯著的提升首屏時間。

完整示例專案

React-New-Features

說明:該專案主要用於React一些新功能及有意思的用法示例,會持續更新,後面會涉及到hooks及最新原始碼相關,有興趣的可以star。

擴充

針對SSR(服務端渲染)應用匯入效能優化

可以使用 Next.js ,Next.js已支援直接使用dynamic import,並且可以指明什麼時候動態匯入(服務端/客戶端),Next.js對首屏做了很多優化,首屏時,服務端會尋找到最簡依賴渲染,具體可以點選前面連結檢視文件。示例:

// Next.js 動態匯入
const MyComponent = dynamic(import(`../components/MyComponent`), {
  ssr: false,
  loading: () => false,
});
複製程式碼

針對元件的動態載入

針對圖片的lazy loading

針對動態請求

  • react-request:對於CSR應用,在不用hooks的情況下,也可以不在componentDidMount中處理請求,讓請求更加的優雅。
  • react-async:利用Render Props + React hooks + Context API統一處理請求,支援SSR使用

封裝react-request示例(統一請求處理)

import React from `react`;
import PropTypes from `prop-types`;
import { Fetch } from `react-request`;
// import ReactLoading from `react-loading`;

import config from `../../config`;

const CustomFetch = (props) => {
  const { url, children } = props;
  return (
    <Fetch
      {...props}
      url={config.serverUrl + url}
      transformData={data => data.data}
    >
      {({ fetching, failed, data }) => {
        if (fetching) {
          // 正在請求:可以展示loading動畫
          // return <ReactLoading type="cylon" color="#2db53f" height="10%" width="10%" />;
        }

        if (failed) {
          // 當前元件請求失敗
          // return <h1 className="home">請求失敗...</h1>;
        }

        if (data) {
          // 請求成功
          return children(data);
        }

        return null;
      }}
    </Fetch>
  );
};

CustomFetch.propTypes = {
  url: PropTypes.string.isRequired,
};

export default CustomFetch;
複製程式碼

在元件中使用CustomFetch

  buildLinks = () => (
    <div className="links">
      <CustomFetch
        url="/getLinks"
      >
        {data => data.map((item, index) => (
          <a className="item" href={item.url} key={`link-${index}`}>
            <img src={item.icon} className="icon" alt={item.name} />
            <p>{item.name}</p>
          </a>
        ))}
      </CustomFetch>
    </div>
  );
複製程式碼

後記

其它效能優化方式,以及自己如何實現,可以參考我的上篇文章:大前端效能總結

相關文章