代碼分片:React.lazy和ErrorBoundary的使用

第一個項目中優化性能的時候作了懶加載和代碼分隔,有一點小的收穫。語言很淺顯直白,望各位大神包涵。javascript

1. 爲何要使用react.lazy?

React.lazy屬於代碼分片(code spliting),當時就很不理解所謂的懶加載和代碼分片到底有什麼區別。css

收穫:

相同點:其實懶加載和代碼分片都是按需加載,都能優化頁面的性能html

懶加載:懶加載是在用戶交互層面來按需加載,實現性能優化的。原理是經過檢測某元素或某組件是否在可視範圍內從而決定是否加載渲染該組件。(用scroll監聽事件配合一些計算IntersectionObserver API來判斷該組件或該元素是否可見)java

代碼分片:代碼分片更爲直接,是在js代碼加載層面來實現按需加載的。咱們一般把js打到一個包裏邊,而React.lazy的做用就是把這部分能夠置後加載的內容從包裏拆分出來,打成另外一個包,須要的時候,再去加載這部分js。react

個人使用場景:

商品的詳情頁面,先不加載非首屏的js文件,等到須要的時候再加載,從而優化首屏的加載速度。webpack

效果

代碼分片雖然只是很小的一個改動,可是自從7月5號開始上線了這個優化之後,效果仍是很明顯的。git

性能提高

2. 如何使用React.lazy和ErrorBoundary

第1步:使用React.lazy引入須要置後加載的部分。 React.lazy接受一個函數,這個函數須要動態import一個React組件。這個函數會返回一個Promise,該Promise resolve的就是咱們但願稍後加載的React組件。github

第2步:必須給置後加載的部分包裹一層Suspense,不然會報錯。Suspense有一個fallback屬性,接受一個React Component。至關於一個placeholder。在沒加載出來這部份內容時臨時佔位的UI。能夠是loading組件或者一個<Fragment/>web

// 第0步:引入React庫中的lazy和Suspense
import React, { Component, lazy, Suspense, Fragment } from 'react';
// 頁頭
import Header from 'containers/product-store-header';
// 首屏
import FirstScreen from 'containers/first-screen';

import './index.scss';

// 第1步:用React.lazy導入須要置後加載的部分
const RestPart = lazy(() => import('containers/rest-part'));

class App extends Component {
    render() {
        return (
            <div className="detail-wrap"> <Header /> <FirstScreen /> <!--第2步:包裹上Suspense,fallback設爲空即<Fragment />--> <Suspense fallback={<Fragment />}> <RestPart /> </Suspense> </div>
        );
    }
}
export default App;

複製代碼

第3步:給Suspense外再包裹一層錯誤邊界ErrorBoundary。爲何要這層錯誤邊界呢?由於若是這部分稍後加載的js出了問題(網絡緣由),沒能成功加載,會致使整個頁面崩潰。錯誤邊界的做用有點像catch,能夠捕獲子組件的錯誤。即使稍後加載的這部份內容有問題,也會顯示ErrorBoundary裏設定的降級的UI而不會致使整個頁面崩潰。性能優化

3.1 寫一個ErrorBoundary組件。

import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';

class ErrorBoundary extends Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    static propTypes = {
        children: PropTypes.element
    }

    static getDerivedStateFromError(error) {
        // 更新 state 使下一次渲染可以顯示降級後的 UI
        return { hasError: true };
    }

    componentDidCatch(error, errorInfo) {
        // 將錯誤日誌上報給服務器
        // some code here...
    }

    render() {
        const { hasError } = this.state;
        const { children } = this.props;
        
        // 這裏個人自定義降級後UI爲空即<Fragment />
        // 你能夠自定義降級後的 UI 並渲染
        return hasError ? <Fragment /> : children; } } export default ErrorBoundary; 複製代碼

3.2 包裹上錯誤邊界

// 第0步:引入React庫中的lazy和Suspense
import React, { Component, lazy, Suspense, Fragment } from 'react';
// 頁頭
import Header from 'containers/product-store-header';
// 首屏
import FirstScreen from 'containers/first-screen';

import './index.scss';

// 第1步:用React.lazy導入須要置後加載的部分
const RestPart = lazy(() => import('containers/rest-part'));

class App extends Component {
    render() {
        return (
            <div className="detail-wrap"> <Header /> <FirstScreen /> <!--第3步:包裹一層錯誤邊界--> <ErrorBoundary> <!--第2步:包裹上Suspense,fallback設爲空即<Fragment />--> <Suspense fallback={<Fragment />}> <!--第1步:React.lazy引入的須要稍後加載的部分 --> <RestPart /> </Suspense> </ErrorBoundary> </div>
        );
    }
}
export default App;

複製代碼

至此,React.lazy和ErrorBoundary的一個簡單的使用就完成了。

3.踩過的坑

3.1 webpackChunkName頗有用

由於要作A/Btest,因此我打包了兩份代碼,因爲兩份代碼配置不一樣,有兩個webpack的實例。因而在本地測試代碼分片的時候,上邊的代碼就出問題了。緣由是本地打包的時候,js文件名是沒有哈希值後綴的,致使AB兩份代碼對於非首屏的js代碼打包後名字相同,加載的時候就報錯了。解決方法十分簡單,給每份用react.lazy引入的組件添加一個不一樣的webpackChunkName就能夠了。

// 會報錯的寫法
const RestPart = lazy(() => import('containers/rest-part'));

// 改進後的寫法 A代碼裏
const RestPart = lazy(() => import(/* webpackChunkName: "rest" */'containers/rest-part'));

//改進後的寫法 B代碼裏
const RestPart = lazy(() => import(/* webpackChunkName: "rest-b" */'containers/rest-part'));
複製代碼

3.2 若是利用代碼分片異步加載React Component

若是是由於網絡緣由致使非首屏的js加載出錯,但咱們但願能顯示一個按鈕,點擊一下,能夠從新加載一次出錯的資源。該怎麼辦呢?

  • 最low的寫法: 固然是location.reload()刷新整個頁面了。
  • 推薦的寫法: 因爲已經出錯的React Component部分返回的是一個Promise,已是reject狀態了,若是放在ErrorBoundary外部import,ErrorBoundary接收到的永遠都是錯誤的狀態。可是若是把動態引入和ErrorBoundary裏邊,則會從新import一次。此時,咱們只需更新ErrorBoundary裏hasError的狀態便可。
import React, { Component, Fragment, lazy, Suspense } from 'react';
import PropTypes from 'prop-types';

class RestWithErrorBoundary extends Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    static propTypes = {
        children: PropTypes.element
    }

    static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以顯示降級後的 UI
        return { hasError: true };
    }

    componentDidCatch(error, errorInfo) {
         // 將錯誤日誌上報給服務器
        // some code here...
    }

    reloadJs() {
        this.setState((state) => {
            return { hasError: state.false };
        });
    }

    render() {
        const { hasError } = this.state;
        const RestPart = lazy(() => import(/* webpackChunkName: "rest" */ 'containers/rest-part'));
        const reloadButton = (<button onClick={this.reloadJs.bind(this)}>Click to reload</button>);
        return hasError ?
            reloadButton :
            <Suspense fallback={<Fragment />}> <RestPart /> </Suspense>;
    }
}

export default RestWithErrorBoundary;

複製代碼

4. 注意事項

  1. React.lazy 和 Suspense 技術還不支持服務端渲染。 若是你想要在使用服務端渲染的應用中使用,請參考 Loadable Components 這個庫。
  2. 錯誤邊界的工做方式相似於 JavaScript 的 catch {},不一樣的地方在於錯誤邊界只針對 React 組件。只有 class 組件才能夠成爲錯誤邊界組件。且錯誤邊界不能捕獲自身的錯誤,只能捕獲其子組件的錯誤。

5. 參考資料 --- React官網

1.代碼分片/code spliting

2.錯誤邊界/error boundary

相關文章
相關標籤/搜索