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

2,836 阅读5分钟

第一个项目中优化性能的时候做了懒加载和代码分隔,有一点小的收获。语言很浅显直白,望各位大神包涵。

1. 为什么要使用react.lazy?

React.lazy属于代码分片(code spliting),当时就很不理解所谓的懒加载和代码分片到底有什么区别。

收获:

相同点:其实懒加载和代码分片都是按需加载,都能优化页面的性能

懒加载:懒加载是在用户交互层面来按需加载,实现性能优化的。原理是通过检测某元素或某组件是否在可视范围内从而决定是否加载渲染该组件。(用scroll监听事件配合一些计算IntersectionObserver API来判断该组件或该元素是否可见) 举个🌰:比如只请求用户可见范围内的图片,其余图片先不请求。

代码分片:代码分片更为直接,是在js代码加载层面来实现按需加载的。我们通常把js打到一个包里边,而React.lazy的作用就是把这部分可以置后加载的内容从包里拆分出来,打成另一个包,需要的时候,再去加载这部分js。 举个🌰:比如整个非首屏的js代码在用户还未浏览非首屏的时候不请求加载。

我的使用场景:

商品的详情页面,先不加载非首屏的js文件,等到需要的时候再加载,从而优化首屏的加载速度。

效果

代码分片虽然只是很小的一个改动,但是自从7月5号开始上线了这个优化以后,效果还是很明显的。

性能提升

2. 如何使用React.lazy和ErrorBoundary

第1步:使用React.lazy引入需要置后加载的部分。 React.lazy接受一个函数,这个函数需要动态import一个React组件。这个函数会返回一个Promise,该Promise resolve的就是我们希望稍后加载的React组件。

第2步:必须给置后加载的部分包裹一层Suspense,否则会报错。Suspense有一个fallback属性,接受一个React Component。相当于一个placeholder。在没加载出来这部分内容时临时占位的UI。可以是loading组件或者一个<Fragment/>

// 第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