带你真正用起来React lazy

14,160 阅读5分钟

React v16.6 版本引入了React.lazy,Suspense的功能,这个功能主要是利用了webpack对es6的import动态载入组件,可以自动实现Code Splitting

Code Splitting就是把代码分成很多个小块(chunk),需要某部分代码的时候再去加载,减小了页面首屏进来加载很大一个js文件的压力,另外拆分成小块还能更好的利用浏览器缓存,下次再用到的话直接从浏览器缓存中读取。

简单使用

Before

import OtherComponent from './OtherComponent';

function MyComponent() {
  return (
    <div>
      <OtherComponent />
    </div>
  );
}

After

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

function MyComponent() {
  return (
    <div>
      <OtherComponent />
    </div>
  );
}

React.lazy接收一个函数作为参数,该函数必须通过调用import来返回一个Promise来加载组件,返回的Promise对象的resolve方法接收组件默认导出的模块

如果只是这么使用运行会出现错误,根据提示需要引入Suspense组件

WechatIMG592.png

Suspense类似于一个错误捕获器,允许定义一个fallback指示符,fallback用来定义我们在等待加载时显示的一些内容。可以将Suspense组件放在动态加载组件上方的任何位置,如果动态组件未加载,则从该组件开始向上寻找,直到找到Suspense组件。可以使用单个Suspense组件包裹多个动态加载组件

完整示例

const OtherComponent = React.lazy(() => import(/* webpackChunkName:"OtherComponent" */'./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<Loading/>}>
      <p>下面是一个动态加载的组件</p>
      <OtherComponent />
      </Suspense>
    </div>
  );
}

真正项目中应用会遇到的问题总结

改造之前的文件大小

WechatIMG596.png

WechatIMG595.png

因为我的项目是个多页面应用,我接下来会以desktop.js这个入口来演示,其中vendor.dll.js是一个第三方库分离的文件。

可以看到,当前这个js文件大小Gzip压缩之后为603.2kb,没有其他任何异步加载的chunk

使用react lazy异步加载改造

不要忘记修改webpack配置

 output: {
    chunkFilename: "static/js/[name].[chunkhash:8].chunk.js"
  },

如果使用了babel转译,则需要安装babel-plugin-syntax-dynamic-import插件来解析动态import语法,修改babel配置

{
     "plugins": ["syntax-dynamic-import"]
}

看一下打包后的文件大小

WechatIMG597.png

可以看到,desktop.js文件已经缩小到了459.69kb,比之前缩小了150kb(因为我只做了部分组件的异步加载,所以文件大小变化不是太大)。下图的Subtile.chunk是我点击按钮之后进行加载的组件

WechatIMG599.png

提取异步chunk中的公共资源

WechatIMG600.png

为了对比清晰,我这张图只包含了MultiLayoutDialog.chunk(66.84kb)和MultiLayoutPollingDialog.chunk(47.55kb)两个模块

情况不太乐观,两个组件的node_modules中都分别包含antd/lib,rc-tree,rc-animate,这是因为每个chunk都是一个独立的可运行的模块,因此会加载自己的依赖,但是显然这是不合理的,应该将这些公共的依赖抽离出来,这个时候你需要用到CommonsChunkPlugin,配置async,用于从异步chunk中抽离公共的资源为一个单独的文件,当首个异步chunk被加载时会同步加载该公共资源文件

重新配置webpack

new webpack.optimize.CommonsChunkPlugin({
      async: "common-in-lazy",
      minChunks: ({ resource }) =>
        resource && resource.indexOf("node_modules") >= 0 && resource.match(/\.js$/),
      children: true,
    })
    // 在所有的async chunk中查找被其中至少2个chunk所共享的node_modules资源,将它们挪到common-in-lazy文件中,没有则新建。

再来看提取了公共资源之后的打包文件大小

WechatIMG606.png

MultiLayoutDialog.chunk(19.85kb)和MultiLayoutPollingDialog.chunk(26.21kb)两个chunk分别减少了46kb和20kb,(另外还有一些其他的chunk体积也减少了,在此没有截图),公共资源抽离为了common-in-lazy-desktop.chunk,该chunk中包含了之前重复加载的antd/lib,rc-tree,rc-animate等资源,改文件大小为50.09kb

拯救丢失的css

不要以为到此异步加载就大功告成了,如果你敢build之后部署,你会收到qa小姐姐亲切问候的,因为有些异步加载的组件样式丢失啦,导致页面显示出现问题。正在喝茶的你吓得立马坐正一波谷歌之后,决定将异步chunk的css样式与入口的css合并到一个css文件,只做js的异步chunk加载,修改webpack配置

 new ExtractTextPlugin({
      filename: cssFilename,
      allChunks: true // 默认为false,仅从初始chunk中提取
    }),

至此,整个异步加载已经全部完成,继续喝茶,总结一下:

1. react的lazy方法需要配合Suspense组件来使用,可以定义异步chunk加载未完成之前的UI显示  

2.webpack配置的出口要添加chunkFilename: "static/js/[name].[chunkhash:8].chunk.js"  

3.如果使用了babel,为避免babel将异步import转译,则需要安装额外的插件来识别动态import  

4.打包的异步chunk会各自包含自己的依赖,这些依赖会存在重复加载,可以使用commonsChunksPlugin将异步chunk中的公共资源提取为一个独立的异步chunk  

5.如果异步chunk的部分样式有丢失,将extractTextPlugin中chunks配置为all,则将所有chunk中的css样式抽离为一个文件,(但是这样做的话,css不会实现异步加载了,感觉不太好,如果大家有好的建议欢迎提出)

以上就是我要分享的内容,如果存在错误的理解或者更好的理解方法,欢迎提出指正。