用React.lazy和Suspense优化React代码打包

Posted Qunar技术沙龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用React.lazy和Suspense优化React代码打包相关的知识,希望对你有一定的参考价值。

用React.lazy和Suspense优化React代码打包

钟志,2013年加入去哪儿网技术团队,目前在大住宿事业部,技术委员会委员,大前端负责人。个人对移动端技术、工程化有浓厚兴趣。


前言

为了让网站加载更快,通常会将前端代码打包。React 在16.6提供 React.lazy 和 Suspense 打包优化方案,本文将介绍 React 代码打包和懒加载相关的技术。

用React.lazy和Suspense优化React代码打包

React 16.6 引入了一些新特性,在 React 组件上使用这些新特性只需要用少量代码就能完成强大的功能。 React.lazy 和 Suspense 是其中的两个新特性,它们让 React 组件的代码拆分和懒加载变得非常简单。 本文重点介绍这两个新特性如何在 React 应用程序中使用,以及它们为 React 开发人员提供的新潜力。

为什么需要拆分代码

在过去的几年中,前端开发发生了很大的变化,随着一些新技术(如 ES6 模块化、Babel 转换器、Webpack 打包工具)的出现,现在可以用完全模块化的方式来开发 javascript 应用。 通常,每个模块都被导入并合并到一个名为 bundle 的单个文件中,然后 bundle 被包括在一个网页上以加载整个应用程序。然而,随着应用程序的增长,包大小开始变得太大,从而开始影响页面加载时间。 像 Webpack 和 Browserify 这样的打包工具提供了 code splitting 对代码拆分的支持,它包括将代码拆分为不同的 bundle ,这些 bundle 可以懒加载,而不是一次全部加载,从而提高了应用程序的性能。

利用import()异步加载

拆分代码的主要方式之一是利用 import() 语法动态导入,该语法还只是一个提议,这个提议为 ES 模块增加了一个新特性,允许我们异步地定义代码依赖。 在不支持 Promise 的老版本浏览器中,需要在网页中增加一个 es6-Promise 的 polyfill。 Webpack 根据 ES2015 loader 规范实现了用于动态加载的 import() 方法,这个功能可以实现懒加载代码,并且使用了 Promise 式的回调,获取加载的包。 在代码中所有被 import() 的模块,都将打成一个单独的包,放在 chunk 存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。

 
   
   
 
  1. import(/* webpackChunkName: "moment" */ 'moment')

  2.  .then(({ default: moment }) => {

  3.    const tomorrow = moment().startOf('day').add(1, 'day');

  4.    return tomorrow.format('LLL');

  5.  })

  6.  .catch(error => console.error('载入模块时发生错误'))

异步加载React组件

下面的代码会一次性全部发送到浏览器:

 
   
   
 
  1. import Description from './Description';


  2. function App() {

  3.  return (

  4.    <div>

  5.      <h1>My Movie</h1>

  6.      <Description />

  7.    </div>

  8.  );

  9. }

接下来将用这段代码改造成 React 异步组件:

 
   
   
 
  1. const LoadDescription = () => import('./Description');


  2. class App extends React.Component {

  3.  state = {

  4.    Description: null,

  5.  };


  6.  componentDidMount() {

  7.    LoadDescription.then(Description => {

  8.      this.setState({ Description: Description.default });

  9.    });

  10.  }


  11.  render() {

  12.    const { Description } = this.state;

  13.    return (

  14.      <div>

  15.        <h1>My Movie</h1>

  16.        {Description ? <Description /> : 'Loading...'}

  17.      </div>

  18.    );

  19.  }

  20. }

这段代码不复杂,与之前代码相比,组件的加载方式变成了异步加载,当组件异步加载成功时,会更新页面的 state ,从而出发页面刷新,刷新后的页面会用 Description 组件代替默认的 Loading... 文本。

用React-loadable改进React组件加载

React 拥有强大生态圈,已经有人针对上面的代码封装了一个代码拆分的组件——React-loadable,它提供了一个高阶组件来利用动态加载 React 组件。用 React-loadable 来简化刚才的代码:

 
   
   
 
  1. import Loadable from 'react-loadable';


  2. const LoadableDescription = Loadable({

  3.  loader: () => import('./Description'),

  4.  loading() {

  5.    return <div>Loading...</div>;

  6.  },

  7. });


  8. function App() {

  9.  return (

  10.    <div>

  11.      <h1>My Movie</h1>

  12.      <LoadableDescription />

  13.    </div>

  14.  );

  15. }

这样好多了吧!既然一切看来那么完美,那为什么还需要 React.lazy 呢? React-loadable 是以组件为单位工作,每个需要异步加载的组件必须定义自己的加载状态。假如页面上有多个异步加载组件时,用户可能看到满屏都是 loading 图标,这不是最好的用户体验。

使用React.lazy和Suspense

在 React 16.6中,通过 React.lazy() 和 React.Suspense 添加了对基于组件的代码拆分和延迟加载的支持。 React.lazy 和 Suspense 暂不支持服务器端渲染,如果你想在一个服务器端渲染的工程中使用代码拆分,推荐你使用 Loadable Components,它有一个很好的使用指南教你如何在服务器端渲染时使用代码拆分。

React.lazy()

React.lazy() 使创建使用动态 import() 加载但像常规组件一样呈现的组件变得容易,在组件渲染时,会自动加载包含组件的包。 React.lazy() 接受一个函数作为它的参数,该函数必须通过调用 import() 来返回一个 Promise 来加载组件。返回的 Promise 对象的 resolve 方法接收组件的默认导出的模块。 下面是 React.lazy() 的用法:

 
   
   
 
  1. import React from 'react';


  2. const Description = React.lazy(() => import('./Description'));


  3. function App() {

  4.  return (

  5.    <div>

  6.      <h1>My Movie</h1>

  7.      <Description />

  8.    </div>

  9.  );

  10. }

其实上面的代码运行会出错,提示需要添加 Suspense 组件:

用React.lazy和Suspense优化React代码打包

Suspense

在 App 组件渲染的过程中,会触发 Description 加载,当 Description 还未加载完成时,最好给用户展示一个 loading,这个工作就是 Suspense 组件干的。在刚才的代码中加上 Suspense ,完整代码如下:

 
   
   
 
  1. import React, { Suspense } from 'react';


  2. const Description = React.lazy(() => import('./Description'));


  3. function App() {

  4.  return (

  5.    <div>

  6.      <h1>My Movie</h1>

  7.      <Suspense fallback="Loading...">

  8.        <Description />

  9.      </Suspense>

  10.    </div>

  11.  );

  12. }

现在通过抓包来验证一下代码拆分结果如何,如下图所示,JavaScript 代码被拆到了2个 js 文件中: (1)main.js:主文件 (2)0.js:使用 React.lazy() 拆分出来的 js

用React.lazy和Suspense优化React代码打包

fallback 属性接收一个 React 元素,这个元素展示组件加载过程中的 loading 信息。Suspense 组件可以是 React.lazy() 组件任意一级父组件,也可以在一个 Suspense 里多个包含多个 React.lazy() 组件,它会捕获所有的 React.lazy() 实例并渲染一次 fallback ,注意,只渲染一次 fallback 。可以将多个异步组件放在同一个 Suspense 中,解决前面说的 React-loadable loading 太多的问题。

 
   
   
 
  1. import React, { Suspense } from 'react';

  2. const Description = React.lazy(() => import('./Description'));


  3. function App() {

  4.  return (

  5.    <div>

  6.      <h1>My Movie</h1>

  7.      <Suspense fallback="Loading...">

  8.        <Description />

  9.        <div>

  10.          <span>Cast</span>

  11.          <AnotherLazyComponent />

  12.        </div>

  13.      </Suspense>

  14.    </div>

  15.  );

  16. }


  17. const AndYetAnotherLazyComponent = React.lazy(() =>

  18.  import('./AndYetAnotherLazyComponent')

  19. );


  20. function AnotherLazyComponent() {

  21.  return (

  22.    <div>

  23.      <span>So...so..lazy..</span>

  24.      <AndYetAnotherLazyComponent />

  25.    </div>

  26.  );

  27. }

上面的代码虽然一个Suspense里多个包含多个React.lazy()组件,但Loading...会一直显示,直到Description和AnotherLazyComponent加载完成。 再来看看当一个Suspense嵌套另一个Suspense会怎样:

 
   
   
 
  1. function App() {

  2.  return (

  3.    <div>

  4.      <h1>My Movie</h1>

  5.      <Suspense fallback="Loading...">

  6.        <Description />

  7.        <div>

  8.          <Suspense fallback="Sorry for our laziness">

  9.            <span>Cast</span>

  10.            <AnotherLazyComponent />

  11.          </Suspense>

  12.        </div>

  13.      </Suspense>

  14.    </div>

  15.  );

  16. }

上面的代码是 Suspense 嵌套的层级。我运行了这段代码,并录了个视频,视频中包含 AnotherLazyComponent 的 0.js 延迟了 3 秒才加载。 当 AnotherLazyComponent 加载时,可以看到 Description 和 Sorry for our laziness。这意味着在 AnotherLazyComponent 加载过程中,不会影响 Description 组件渲染。页面会以 Suspense 为边界切割成一个个的小区域,每个小区域渲染都是独立的。

用React.lazy和Suspense优化React代码打包

结束语

你可以更新到 React 16.6 来体验 React.lazy() 和 React.Suspense ,使用它们 React 组件的代码拆分和懒加载变得非常简单。 本文所用的代码可以在 github 下载:https://github.com/zhongzhi107/react-lazy-demo

相关链接

https://reactjs.org/blog/2018/10/23/react-v-16-6.html https://hswolff.com/blog/react-lazy-and-suspense/ http://www.ptbird.cn/react-lazy-suspense-error-boundaries.html https://reactjs.org/docs/code-splitting.html https://webpack.js.org/guides/code-splitting/ https://github.com/jamiebuilds/react-loadable

以上是关于用React.lazy和Suspense优化React代码打包的主要内容,如果未能解决你的问题,请参考以下文章

React.lazy和React.Suspense异步加载组件

react lazy和suspense

React中异步模块api React.lazy和React.Suspense

如何在 React.lazy 和 Suspense 中获取加载进度

react懒加载(lazy, Suspense)

react懒加载(lazy, Suspense)