umi v3配置 优化打包体积+提升缓存效率

最近一直在看umi打包相关的优化,踩了好多坑,总算有了一点小小的心得,在此分享一下,希望能帮到有类似需要的朋友们。

我做的优化主要分成两个方面:

  1. 优化打包体积,即减少打包产物的大小,减少用户需要加载的文件体积,提升网站的加载速度
  2. 提升缓存效率,用户在首次访问网站之后,浏览器会缓存加载过的jscss等资源文件。我们发布新版本时,应该尽可能少地更新文件,以最大程度地利用用户浏览器本地的缓存。降低用户二次或多次访问网站的成本。

打包产物分析

在开始优化之前,我们首先要分析一下现有的打包产物,看看有哪些问题,再考虑如何解决、优化它们。

umi项目已经配置好了分析所用的插件 (webpack-bundle-analyzer),直接运行npm run analyze命令即可。

运行完成之后会自动打开打包产物分析页面。这是我的项目优化之前的打包结果,目前所有打包产物体积一共是4.23mb。

接下来,我们就一起分析一下,目前打包存在的问题,以及如何优化它们。

第三方依赖被重复打包

qrcode这个依赖为例,我们在打包结果中搜索它,可以看到有不止一个页面文件都包含了它。说明这个依赖被重复打包到多个文件里了。

如果你没有对umi中的配置进行过任何更改,完全使用默认的配置,umi会把每个页面中用到的依赖各自打包到对应页面的打包产物中。这使得如果在不同页面间使用了相同的依赖,会导致这个依赖被重复打包多次。

一个依赖被重复打包显然会造成打包产物的冗余,其实理想情况中,我们希望这些依赖只被打包一次,放到一个公共的文件中,需要使用它的页面都去引用这个文件就好了。

为了实现这种理想情况,我们需要配置中webpack中的optimization.splitChunksumi中修改webpack配置需要通过chainWebpack,具体配置如下:

javascript 复制代码
{
  //...other umi config
  chainWebpack: function (config, { webpack }) {
    config.merge({
      minimize: true,
      optimization: {  //webpack配置
        splitChunks: {
          chunks: 'all',
          minSize: 20000,
          minChunks: 2,
          cacheGroups: {
            vendors: {
              name: 'vendors',
              test({ resource }) {
                return /[\\/]node_modules[\\/]/.test(resource);
              },
              priority: 10,
            },
          },
        },
      },
    }); 
}

这段配置的关键在于optimization.splitChunks.cacheGroups.vendors,它的作用是告诉webpack,只要是在/node_modules目录下,被引用次数大于等于2次的模块,全都打包到一个叫vendors的文件中,而不是像之前那样,被打包到不同的页面文件中。

这样我们就实现了把公共依赖提取到一个单独的文件,以供其他页面引用的目的。

之后,不要忘了在chunks配置中加上我们刚刚抽离出来的vendors 文件。chunks代表初始加载时需要加载的文件。vendors包含了所有的第三方库,所以初始加载时肯定是需要的。

javascript 复制代码
{
  //...other umi config
  chunks: ['vendors', 'umi'],
}

现在我们再来npm run analyze一下, 看看打包产物优化的效果:

可以看到,qrcode这个依赖只被打包了一次,被放到vendors.js 中。并且打包产物的总体积也从4.23mb降低到了3.48mb。这一部分体积的减少,实际就是之前被重复打包的依赖。比如还有echarts等等。

按需引入echarts

在React项目中使用echarts,我自己用的是echarts-for-react这个库。之前的引入方式是这样的:

javascript 复制代码
import ReactECharts from 'echarts-for-react';

//...
<ReactECharts option={option} />

这样的引入方式,使用是没问题,但是会造成打包产物的冗余。因为echarts-for-react不支持tree-shaking。按上面的方式引入,即使我们只使用了其中的一两种图表类型,比如我的项目里只用到了折线图和柱状图,也会把整个echarts-for-react(包含了整个echarts)都打包。

可以看到,vendors.js 的体积为559kb,其中echarts-for-react的体积占了其中的大部分。

我们可以通过按需引入的方式,只import我们需要的部分组件,来避免打包其他的、并不会用到的echarts代码。以降低打包产物的体积。

比如像下面这样:

javascript 复制代码
import ReactEChartsCore from 'echarts-for-react/lib/core';
import { BarChart } from 'echarts/charts';    //只使用柱状图
import { GridComponent, TooltipComponent, LegendComponent, LegendScrollComponent } from 'echarts/components';
import * as echarts from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';

echarts.use([LegendComponent, LegendScrollComponent, TooltipComponent, GridComponent, BarChart, CanvasRenderer]);

//...
<ReactEChartsCore echarts={echarts} option={option} />

echarts-for-react的使用都改成按需引入的方式以后,再打包:

可以看到vendors.js 的体积从559kb降低到了422kb,这减少的体积就是echarts-for-react中多余的文件体积。

减少首屏需要加载的文件体积

除了打包产物总的体积以外,我们还需要关注的是用户打开网站,加载第一个页面所需要下载的文件体积。除了相应的页面文件以外,vendors.jsumi.js ,是用户不管打开哪个页面,都需要下载的文件。我们可以尝试一下继续降低vendors.js的体积。

我们查看vendors.js 的组成,可以看到,echarts-for-react / zrender / brace / react-json-view 这些第三方库占了其中的绝大部分体积。这些库其实在加载大部分页面的时候都不需要用到,所以没有必要放到vendors.js中。

我们需要把它们从vendors.js中拆分出来,拆成单独的文件,用户访问到使用这些库的页面时,再去加载它们。

至于配置,又要用到optimization.splitChunks.cacheGroups。这次,我们把配置换成下面这样子:

javascript 复制代码
{
  //...other umi config
  chunks: ['vendors', 'umi'],
  chainWebpack: function (config, { webpack }) {
    config.merge({
      optimization: {
        minimize: true,
        concatenateModules: true,
        splitChunks: {
          chunks: 'all',
          minSize: 20000,
          minChunks: 2,
          cacheGroups: {
            vendors: {
              test: /[\\/]node_modules[\\/]/,
              name(module) {
                const reg = /(echarts|zrender|brace|react-json-view)/;
                if (reg.test(module.identifier())) {
                  const [chunkName] = reg.exec(module.identifier()) || [];
                  return `npm.${chunkName}`;
                };
                return 'vendors';
              },
              priority: 10,
            },
          },
        },
      },
    });
  },
}

再次打包,查看打包结果:

可以看到,echarts-for-react / zrender / brace / react-json-view这些第三方库全都被抽离到了单独的文件中。vendors.js的体积也降低到了62kb。

这里请注意 :虽然vendors.js的体积变小了很多,但是打包产物的总体积基本是没变的,因为我们这里只是拆包,把原本放在一起的依赖拆分成多个,对总体积的影响是很小很小的。

webpack 5打包

umi v3中默认使用webapck 4打包,如果开启配置项webpack5,才会使用webpack 5打包,并且默认开启webpack 5中cache特性,对提升打包速度也很有帮助。

javascript 复制代码
 {
   //...other umi config
   webpack5: {}
 }

让我有点惊喜的是,完全相同的配置,开启webpack5之后,打包产物总的体积有明显的变小。

优化到现在,开启webpack5之前,打包产物总的体积为3.44mb;开启之后,总体积为2.95mb。有明显的降低。


到目前为止,我们可以发现,虽然vendors.js 的体积得到了明显的优化。但是umi.js的体积似乎没什么变化?依然有着1.11mb的大小。

在优化过程中,我发现umi.js 中的第三方依赖,即/node_modules目录下的模块,通过optimization.splitChunks是拆不出来的,给官方提了discussion,也没有得到支持的答复。

umi.js 中包含了多个几乎不更新的第三方依赖,但是当我们更改代码,重新打包,umi.js 的hash值经常会变化。这就导致了一旦我们发布新版本,用户访问网站,需要重新下载umi.js里这些其实并没有发生更新的依赖。

这对于缓存效率是很不利的。umi.js 包含了一些体积很大,项目必需,又很少更新的第三方库,比如:antd / moment / react / react-dom / xlsx (用于导出excel文件)。对于这些库,我们应该考虑把它们拆分出来,并且在用户首次访问之后,尽可能地通过缓存加载,而不是需要频繁地下载它们。

externals

既然通过optimization.splitChunks拆分不出来,那么可以考虑另一种方式:externals

externals用于将一些第三方库排除出打包过程,不出现在最终的打包结果中。再通过其他方式引入它们,比如<script>标签引入。

externals的值是一个对象,其中key的值是第三方库的名字,value的值是通过<script>标签引入的全局变量的名字。

我们把antd / moment / react / react-dom / xlsx全部配置到externals中,并通过cdn引入它们。这样,就可以实现把它们排除出umi.js。每次更新、发布新版本,如果用户之前访问过,有缓存,就不需要重新下载它们了。

具体配置如下:

javascript 复制代码
import * as antd from 'antd';
import _ from 'lodash';

let externalsAntd = {};
for (const key in antd) {
  if (Object.hasOwnProperty.call(antd, key)) {
    externalsAntd[`antd/es/${_.kebabCase(key)}`] = `antd.${key}`;
  }
};

const config = {
  //...other umi config
  externals: {
    react: 'React',
    moment: 'moment',
    xlsx: 'XLSX',
    'react-dom': 'ReactDOM',
    'moment/locale/zh-cn': 'moment.locale',
    ...externalsAntd,
  },
  styles: ['https://cdnjs.cloudflare.com/ajax/libs/antd/4.23.6/antd.min.css'],
  scripts: [
    'https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/antd/4.23.6/antd.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/locale/zh-cn.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.5/xlsx.full.min.js',
  ],
};

可以看到momentantd的配置比较特殊:

moment是因为<DatePicker />组件里引入了moment/locale/zh-cn,如果不配置它,整个moment还是会被打包。详情请见:ant-design issue #4136

antd是因为umi内部使用了babel-plugin-import,所以引入的不是antd,而是antd/es/xxx。详情请见:umi issue #4542

再次打包,查看打包结果:

可以看到,umi.js 中已经没有了antd / moment / react / react-dom / xlsx等依赖,体积也减少到了458kb。

这里请注意 :虽然umi.js 的体积有了明显地降低,但是实际上用户首次访问网站需要加载的文件体积不会减少,甚至还有可能会增加。这是因为externals配置的第三方库,通过cdn引入,是不经过webpack打包的,自然就不支持webpack的tree-shaking。 所以对于那些可以通过optimization.splitChunks单独拆分出来的、支持tree-shaking的依赖,就不建议配置到externals了。

即使首次访问加载的文件体积没有减少,但通过cdn引入的依赖,我们之后每次发布新版本,用户都无需再重新下载依赖文件。这是我们配置externals的目的所在。

runtimeChunk

我们以Tracking_detail这个页面为例,如果修改了这个页面的代码,重新打包。会发现umi.jsTracking_detail.js 的hash值都会改变。这就导致,我们虽然只修改了一个页面的代码,却需要重新下载umi.js + Tracking_detail.js

这是因为默认情况下,umi.js 包含整个应用的runtime和manifestruntime和manifest包含了打包后的带有hash值的文件名。所以只要有一个文件的hash值改变,都会导致umi.js 的内容及其hash值的更新,进而导致用户缓存的umi.js失效,需要重新下载。

如果我们可以把runtime和manifest抽离成单独的文件,每次更改单个页面的代码,就只需要重新下载对应页面的js文件 + 小小的runtime.js ,并使用缓存的umi.js了。

抽离runtime和manifest需要用到webpack的optimization.runtimeChunk,具体配置如下:

javascript 复制代码
{
  //...other umi config
  chunks: ['runtime~umi', 'vendors', 'umi'],  //添加'runtime~umi'
  chainWebpack: function (config, { webpack }) {
    config.merge({
      optimization: {
        runtimeChunk: true,  //添加runtimeChunk
        splitChunks: {
          //...splitChunks config
        },
      },
    });
  },
};

添加完runtimeChunk之后,我们先打包一次,可以看到会生成一个单独的runtime~umi.js 文件,我们记住此时umi.jsTracking_detail.jsruntime~umi.js的hash值:

修改Tracking_detail代码并重新打包:

可以看到,umi.js 的hash值没变,Tracking_detail.jsruntime~umi.js 的hash值有更新,成功地避免了umi.js的缓存失效。

量化缓存效率的提升

做了这些优化,我也假设一个场景,简单算一下,这些优化对缓存真正的提升有多少。

浅算一下哈,文件的大小都是以我自己的这个项目为例。

假设:我们的网站只有Tracking_detail一个页面,用户每周访问一次,我们也每周更新一次Tracking_detail并发布新版本。用户连续五周访问。我们看一下,配置externalsruntimeChunk前后,用户加载所需文件体积的变化:

配置前,五周加载文件体积总计:6.67mb:

配置后,五周加载文件体积总计:1.63mb:

在这个场景下,优化后,加载文件体积从6.67mb降低到1.63mb,降低了74%。(回头周报就这么写✌)

相关推荐
dr李四维10 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
雯0609~31 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ34 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z40 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4041 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five1 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序1 小时前
vue3 封装request请求
java·前端·typescript·vue
临枫5411 小时前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript