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%。(回头周报就这么写✌)

相关推荐
前端爆冲7 分钟前
项目中无用export的检测方案
前端
热爱编程的小曾35 分钟前
sqli-labs靶场 less 8
前端·数据库·less
gongzemin1 小时前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
Apifox1 小时前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
树上有只程序猿1 小时前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼2 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下2 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox2 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞2 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行2 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox