基于vue-cli项目的webpack打包优化实践

4,871 阅读5分钟

前言

看了很多打包优化的文章,很多都是基于原生的webpack配置,直接在webpack.config.js文件中修改配置的。但是vue-cli创建的项目已经封装了基本的webpack配置,需要在vue.config.js文件中修改预置的webpack配置。很少看到这方面的文章,因此记录一下自己的实践过程和踩过的一些坑。

本次使用技术的版本情况:

  • vue:2.6.10
  • @vue/cli:4.0.5
  • webpack:4.31.0

vue-cli中的webpack

要优化项目,首先我们得了解vue-cli已经替我们做过了哪些优化,也就是需要查看webpack已经配置了哪些选项。
使用vue inpsect输出webpack配置,还可以指定输出的文件:vue inspect > output.js

vue-cli提供了两种方式来更改webpack配置:
1、原生配置方式,配置的结果将会被 webpack-merge 合并入最终的 webpack 配置。

// vue.config.js
module.exports = {
    configureWebpack: {
        // 在这里直接书写webpack配置项...
    }
}

2、链式配置方式,vue-cli内部是使用webpack-chain这个插件来维护webpack配置的,因为能更细粒度的控制其内部配置,因此也是官方比较推荐的一个方式。

// vue.config.js
module.exports = {
    chainWebpack: config => {
        config.resolve.alias.set('@assets', resolve(`src/assets`));
    },
}

这两种方法可以配合使用。
为了简便,也为了少踩点儿坑,本次优化主要采用原生的webpack配置,也就是使用configureWebpack的方式。 优化过程分为打包体积优化和打包速度优化。

优化打包体积

使用webpack-bundle-analyzer分析打包体积

webpack官方提供一些插件分析打包性能。

  • webpack-chart:webpack stats 可交互饼图。
  • webpack-visualizer:可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
  • webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为便捷的、交互式、可缩放的树状图形式。
  • webpack bundle optimize helper:此工具会分析你的bundle,并为你提供可操作的改进措施建议,以减少 bundle 体积大小。

我们使用webpack-bundle-analyzer来分析打包体积。

// yarn add analyze-webpack-plugin --dev

// vue.config.js
const AnalyzeWebpackPlugin = require('analyze-webpack-plugin')
module.exports = {
  configureWebpack: {
    plugins: [
      new AnalyzeWebpackPlugin({}),
    ],
  }
}

运行打包命令:yarn build,会自动打开分析结果页面。

webpack-bundle-analyzer使用三种指标衡量打包体积:

  • stat:输入的文件大小,还未经过例如压缩之类的转换。
  • parsed:输出的文件大小,代码经过丑化压缩后的大小。
  • gzip:开启了gzip压缩后的大小。
优化moment —— ContextReplacementPlugin

观察上图,可以发现moment占据了不小的比重,主要是一些本地化的语言包,默认都会打包进来。
对于普通应用来说,我们只需要中文语言包就够了。

优化前:

  • Stat: 540.76KB
  • Parsed: 234.36KB
  • Gzipped: 68.46KB

首先选择合适的语言包设置语言环境

import moment from 'moment';
import 'moment/locale/zh-cn';
moment.locale('zh-cn');

ContextReplacementPlugin插件的作用是改变某个模块的打包上下文,通过修改正则,来让webpack只打包我们想要的文件。

// yarn add webpack --dev

// vue.config.js
const webpack = require('webpack');
module.exports = {
  configureWebpack: {
    plugins: [
      new webpack.ContextReplacementPlugin(
        /moment[/\\]locale$/, // 这个参数表明了我们要改变的打包上下文
        /zh-cn/ // 这个参数表示我们只想打包这个正则匹配的文件
      )
    ]
  },
};

优化后:

  • Stat: 150.79KB
  • Parsed: 54.61KB
  • Gzipped: 17.96KB

原来393.36KB的语言包只保留中文后变为仅有3.39KB。

关于moment打包,社区提供了很多方法,还有其他一些方案可以参考:github.com/jmblog/how-…

优化XLSX

我们项目中使用了这个库来生成excel并下载。
原来直接引入import { utils, writeFile } from 'xlsx';,打包后体积非常庞大。

优化前:

  • Stat: 1.23MB
  • Parsed: 920.85KB
  • Gzipped: 327.65KB

后来在issue区查到了解决方案,改为只引入mini版本:

import { utils, writeFile } from 'xlsx/dist/xlsx.mini.min.js';

如果使用的是typescript会报错:

声明一下模块即可:

// modules.d.ts
declare module 'xlsx/dist/xlsx.mini.min.js';

优化后几乎只剩了零头:

  • Stat: 236.73KB
  • Parsed: 189.66KB
  • Gzipped: 60.79KB

注意,官方解释这个xlsx这么大是有原因的,因为涉及到读取文件,要支持一些比较老的格式。如果你的项目中只是用来生成excel,不涉及读取文件,就可以用这个mini版本;如果有涉及到读取excel文件的操作,还是老老实实全量引入吧。官方未来或许会提供只支持现代文件格式的轻量级版本。

lodash打包体积 —— lodash专用plugin

优化前:

  • Stat: 540.17KB
  • Parsed: 73.29KB
  • Gzipped: 25.74KB

需要使用两个插件:

  • babel-plugin-lodash 用来精简Lodash模块的,只保留用到的方法。

  • lodash-webpack-plugin 这个插件通过用noop, identity, 或其他更简单的替代品来替换一些模块的特性,使得打包后的体积更小(翻译)。
    注意:这个插件默认会关闭一些lodash不常用的特性,可以给插件传递options来开启某些特性。

这两个插件配合使用来使效果最大化。只需要在Babel插件中添加lodash,并在webpack配置中添加一个插件:

// yarn add babel-plugin-lodash lodash-webpack-plugin --dev

// babel.config.js
modules.exports = {
  // 其他配置省略...
  plugins: ['lodash']
}

// vue.config.js
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
module.exports = {
  // ...
  configureWebpack: {
    plugins: [
      new LodashModuleReplacementPlugin()
    ]
  }
}

优化后:

  • Stat: 56.42KB
  • Parsed: 11.34KB
  • Gzipped: 3.81KB

低调了许多,找了好久才找到 XD

抽取公共代码 —— splitChunks(webpack4之前使用commonChunkPlugin,webpack4之后使用splitChunks)

我们项目中使用了西瓜播放器,发现xgplayer作为第三方库,并没有被打包进chunk-vendors,并且还重复打包了两次。

关于这个xgplayer,引用情况是:有两个页面引用了一个公共的组件,这个组件引用了xgplayer。所以为什么xgplayer没有打包进chunk-vendors?

看一下vue-cli预设的webpack配置:

// ...
optimization: {
    minimizer: [
      // ...
    ],
    splitChunks: {
      cacheGroups: {
        vendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 1,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    }
  }

vendors打包了node_modules里符合条件的第三方库,这个条件就是chunks: 'initial'
chunks表示要打包的这些chunks的类型,有三个值:

  • initial:初始的chunk,需要立即加载,其实就是main.ts里通过import同步引入的模块。
  • async:通过import()等动态引入的chunk,也就是按需引入的异步的模块。
  • all:包含同步和异步的模块。选了这个,将会打包所有test匹配到的模块,这里是node_modules,显然是不合适的,因为有些第三方库可能晚点才会用到,比如这里的xgplayer。

所以xgplayer虽然是通过import同步引入的,但引用它的两个页面组件在路由文件中是import()按需引入的,并且没有在main.ts中引入xgplayer,所以自然不会打包到chunk-vendors里。

所以应该按照异步模块async或all的类型来打包。

// vue.config.js
module.exports = {
  // ...
  configureWebpack: {
    optimization: {
      splitChunks: {
        cacheGroups: {
          xgplayer: {
            name: 'xgplayer',
            test: /[\\/]node_modules[\\/]xgplayer[\\/]/,
            minSize: 0,
            minChunks: 1,
            reuseExistingChunk: true,
            chunks: 'all'
          }
        }
      }
    }
  }
}

优化后只打包了一份:

优化echarts —— IgnorePlugin

优化echarts的难点在于,项目前期使用了两种方式:

  1. 引入了第三方的vue-echarts,参考这个组件写了一个自己的公共组件base-chart,结果并没有用到这个库。 页面的图表有的是基于base-chart的同时按需引入相关组件如echarts/lib/line。
  2. 有的是直接引用原生的echart从0开始写的组件。

这就导致了echarts全量引入,并且到处打包的问题。

解决方案:由于我们的首页是登录页,没有用到echarts,不需要第一次就加载echarts,因此要做两件事来优化:

  1. 这个第三方vue-echarts在main.ts中全局注册组件了,但其实并没有使用,需要删除全局引用,避免打包进chunk-vendors。
  2. 抽离重复打包的部分,合并进一个chunk。
// main.ts的组件注册代码也要注释掉
// import ECharts from 'vue-echarts';
// Vue.component('echart', ECharts);

optimization: {
  splitChunks: {
    cacheGroups: {
      echarts: {
        name: 'echarts',
        test: /[\\/]node_modules[\\/]echarts[\\/]/,
        minSize: 0,
        minChunks: 1,
        reuseExistingChunk: true,
        chunks: 'all',
      },
    },
  },
},

经过优化后已经从chunk-vendor里抽离出来,并把多处存在的echarts引用合并进了一个bundle。但是可以看到体积还是很大的。

github上有人就打包体积太大提了issue,作者建议使用在线builder,根据项目使用情况按需打包。并说5.0版本可能会考虑减小打包体积。

但是实际使用过程中打包到中途某些资源504网关超时了,重试了几次都失败,只好另寻他法。

使用webpack内置的IgnorePlugin插件来忽略项目中用不到的文件。可以对照在线builder的网址。
分别从node_modules/echarts/lib目录下的component、chart、coord三个目录进行排除。

IgnorePlugin插件配置项中,需要先使用contextRegExp来确定即将要排除的文件的上下文,这里是echarts目录。
然后使用resourceRegExp来指定要排除的资源的正则表达式。
实际上,这里只排除了这些目录,还有一些跟目录同级的文件,可能跟要排除的这些图表/组件相关,但是为了避免误判,就做不到那么精细了。

plugins: [
  new webpack.IgnorePlugin({
    resourceRegExp:
      /^\.\/lib\/(component\/visualMap|toolbox|timeline|geo|brush|calendar)|(chart\/effectScatter|candlestick|heatmap|tree|treemap|sunburst|map|graph|boxplot|parallel|gauge|funnel|sankey|themeRiver|pictorialBar)|(coord\/polar|geo|singleAxis|calendar)$/,
    contextRegExp: /echarts$/
  })
]

优化后立马小了不少:

优化ant-design-vue

优化前:

发现icons占据很大的位置,但是实际使用的时候极少使用icons。
GitHub上面有人提了issue 作者解释说button会自动引用icon,设计如此。ant-design已经在优化了,目前暂时使用了作者推荐的方法来按需引入icon:
增加一个别名,让webpack解析的时候使用我们提供的icons.js文件中的路径,只打包使用过的icon。

resolve: {
  alias: {
    '@ant-design/icons/lib/dist$': resolve('./src/core/antd/icons.js')
  }
},

然后在src目录下添加相应的文件,见github

export {
  default as SettingOutline
} from '@ant-design/icons/lib/outline/SettingOutline'
export {
  default as GithubOutline
} from '@ant-design/icons/lib/outline/GithubOutline'
export {
  default as CopyrightOutline
} from '@ant-design/icons/lib/outline/CopyrightOutline'

/* MultiTab begin */
export {
  default as CloseOutline
} from '@ant-design/icons/lib/outline/CloseOutline'
export {
  default as ReloadOutline
} from '@ant-design/icons/lib/outline/ReloadOutline'
export {
  default as DownOutline
} from '@ant-design/icons/lib/outline/DownOutline'
export {
  default as AlignLeftOutline
} from '@ant-design/icons/lib/outline/AlignLeftOutline'
/* MultiTab end */

/* Layout begin */
export {
  default as LeftOutline
} from '@ant-design/icons/lib/outline/LeftOutline'
export {
  default as RightOutline
} from '@ant-design/icons/lib/outline/RightOutline'
export {
  default as MenuFoldOutline
} from '@ant-design/icons/lib/outline/MenuFoldOutline'
export {
  default as MenuUnfoldOutline
} from '@ant-design/icons/lib/outline/MenuUnfoldOutline'
export {
  default as DashboardOutline
} from '@ant-design/icons/lib/outline/DashboardOutline'
export {
  default as VideoCameraOutline
} from '@ant-design/icons/lib/outline/VideoCameraOutline'
export {
  default as LoadingOutline
} from '@ant-design/icons/lib/outline/LoadingOutline'
export {
  default as GlobalOutline
} from '@ant-design/icons/lib/outline/GlobalOutline'
export {
  default as UserOutline
} from '@ant-design/icons/lib/outline/UserOutline'
export {
  default as LogoutOutline
} from '@ant-design/icons/lib/outline/LogoutOutline'
/* Layout end */

优化后已经低调了许多:

至此,项目打包已经得到了很大程度的优化,对比优化前,打包的总体积减小了约1/3,压缩后减小了约一半的体积,终于降到了KB级,可喜可贺:

优化前:

  • Stat: 10.54MB
  • Parsed: 4.94MB
  • Gzipped: 1.55MB

优化后:

  • Stat: 7.57MB
  • Parsed: 3.2MB
  • Gzipped: 1003.36KB
总结
  1. 对于首页不需要的模块,尽量不要使用同步引用(import XX from '...')的方式引入到入口文件中,避免打包到一起,以减少首次请求的时间,加快首页的渲染速度;
  2. 使用第三方库的时候尽量按需引入,如果有需要,可以使用IgnorePlugin或ContextReplacementPlugin告诉webpack我们需要/不需要打包的文件;
  3. 使用splitChunks提取公共模块,注意chunks这个属性的值,如果是在按需引入(import())的vue组件中使用同步引入的模块,chunks设置成initial是没用的。这也是为什么vue-cli预设的splitChunks没有帮我们把某些重复代码抽离出来,它只会帮我们处理同步的模块:
// ...
optimization: {
    minimizer: [
      // ...
    ],
    splitChunks: {
      cacheGroups: {
        vendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 1,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    }
}

优化打包速度

使用speed-measure-webpack-plugin插件测量打包各环节耗费时间

vue-cli中的使用方法

// yarn add speed-measure-webpack-plugin --dev

// vue.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

module.exports = {
  // 这里无法使用链式写法chainWebpack,会报错
  configureWebpack: smp.wrap({
    // ... webpack config goes here ...
  }
}

运行打包指令:yarn build

使用dll提取不常更新的公共库

更新中...