Vue-Cli优化编译速度

前言

Vue-Cli中内置了Webpack,但是配置文件和Webpack也不尽相同。

我们可以通过命令查看对应的Webpack配置。

对于优化主要是两个方面

  • 构建速度
  • 打包体积

所以不管是分析问题还是解决问题有围绕这连个方面进行处理。

Vue-Cli自带

  • cache-loader 会默认为 Vue/Babel/TypeScript 编译开启。文件会缓存在 node_modules/.cache 中。

    如果你遇到了编译方面的问题,记得先清缓存目录之后再试试看。

  • thread-loader 会在多核 CPU 的机器上为 Babel/TypeScript 转译开启。

查看Vue-Cli中的Webpack配置

介绍

Vue-Cli脚手架会有webpack的很多默认行为,因此我们得知道基于Vue-Cli的项目,当前的webpack都配置了啥,然后才能做针对性的分析与优化。

vue-cli-service 暴露了 inspect 命令用于审查解析好的 webpack 配置。那个全局的 vue 可执行程序同样提供了 inspect 命令,这个命令只是简单的把 vue-cli-service inspect 代理到了你的项目中。

使用方式:

1
2
3
#根据mode,分别生成开发环境、生产环境的配置
vue inspect --mode production > webpack.config.production.js
vue inspect --mode development > webpack.config.development.js

输入命令后,在根目录会生产一个webpack.config.production.js文件

如果vue command not found的错可以全局安装注册一下vue命令

1
npm install -g vue-cli

分析

查看构建时间

说明:https://www.npmjs.com/package/speed-measure-webpack-plugin

安装

1
npm install --save-dev speed-measure-webpack-plugin

使用

1
2
3
4
5
6
7
8
9
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
module.exports = {
chainWebpack: config => {
config
.plugin('speed-measure-webpack-plugin')
.use(SpeedMeasurePlugin)
.end();
}
}

或者

1
2
3
4
5
6
7
8
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
module.exports = {
configureWebpack: (config) => {
config.plugins.push(
new SpeedMeasurePlugin(),
);
},
};

查看构建库大小

VUE CLI内置工具

1
vue-cli-service build --report

成功后就会在项目目录下找到/dist/report.html

结果如下图所示:

image-20220825103406352

如果报错

node_modules.bin\vue-cli-service.ps1,因为在此系统上禁止运行脚本。

执行之后就能运行上面的命令了

1
2
3
set-ExecutionPolicy RemoteSigned -Scope CurrentUser
#查看执行权限
Get-ExecutionPolicy

webpack-bundle-analyzer

Vue CLi就不用这个工具了,但是也可以配置,配置后运行项目打开项目页面的同时也会打开分析页面。

安装

1
npm install --save-dev webpack-bundle-analyzer

配置

Webpack配置

1
2
3
4
5
6
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}

Vue Cli配置

1
2
3
4
5
6
7
8
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
configureWebpack: {
plugins: [
new BundleAnalyzerPlugin()
]
}
}

优化

构建速度优化

happypack

安装

1
npm install --save-dev happypack

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
happypack
*/
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
configureWebpack: config => {
return {
/* happypack */
module: {
rules: [
{
test: /\.js$/,
loader: 'happypack/loader?id=happyBabel',
exclude: /node_modules/
},
]
},
externals: {
vue: "Vue",
"vue-router": "VueRouter",
vuex: "Vuex",
echarts: "echarts",
axios: "axios",
},
plugins: [
new HappyPack({
id: 'happyBabel',
loaders: [{
loader: 'babel-loader?cacheDirectory=true',
}],
//共享进程池
threadPool: happyThreadPool,
//允许 HappyPack 输出日志
verbose: true,
})
],
}

},
}

实测没啥效果

vue-cli-plugin-dll

1
npm install --save-dev vue-cli-plugin-dll

接下来就是dll的相关配置,将我们项目中的依赖使用dll插件进行动态链接,这样依赖就不会进行编译,从而极大地提高编译速度,因为这些插件没有编译,在vue.config.js中进行配置,也很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const path = require("path");
module.exports = {
pluginOptions: {
dll: {
//这里放的是你的依赖插件,就是你项目安装的其他的插件,将这些插件的名字依次加在后面,我建议将所有项目依赖插件全部放在后面
//注意这里不能放webpack,gulp等需要node环境的插件,我尝试将babel等放到这里报错提示没有V8环境
entry: ["vue", "vue-router", "view-design"],
//dll 编译后的链接库的地址
cacheFilePath: path.resolve(__dirname, "./public"),
// 是否开启 DllReferencePlugin,
open: true,
// 在执行 `dev` , `build` 等其他指令时,程序会自动将 `dll` 指令生成的 `*.dll.js` 等文件自动注入到 index.html 中。
inject: true
}
},
}

多入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const path = require('path')
module.exports = {
pluginOptions: {
dll: {
// 入口配置
entry: {
vue: ["vue", "vue-router", "vuex"],
ui: ["view-design"],
},
// 输出目录
output: {
path: path.join(__dirname, 'public/dll'),
filename: '[name].dll.js',
// vendor.dll.js中暴露出的全局变量名
// 保持与 webpack.DllPlugin 中名称一致
library: '[name]_[hash]'
},
// 是否开启 DllReferencePlugin,
open: true,

// 在执行 `dev` , `build` 等其他指令时,程序会自动将 `dll` 指令生成的 `*.dll.js` 等文件自动注入到 index.html 中。
inject: true,
}
}
}

配置好之后然后运行,进行你上面配置插件动态链接库的编译

1
npx vue-cli-service dll

dll编译完成后会在上面配置的目录下生成dll文件夹,就可以开始跑项目了,因为这些插件都不需要编译,跑起来很流畅,修改后的热更新速度更是显著提升。我以前修改一行代码热更新编译在30秒以上,使用这个以后基本十秒以内搞定。

1
npm run serve

多线程优化

Vue-Cli

Vue-Cli已经内置,开启

1
2
3
module.exports = {
parallel: true,
}

parallel

  • Type: boolean

  • Default: require('os').cpus().length > 1

    是否为 Babel 或 TypeScript 使用 thread-loader

    该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。

Webpack

1
npm install --save-dev thread-loader

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require("path");
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
"thread-loader",
// 耗时的 loader (例如 babel-loader)
],
},
],
},
};

缓存构建

Webpack 中几种缓存方式:

  • cache-loader
  • hard-source-webpack-plugin

以上这些缓存方式都有首次启动时的开销,即它们会让 “冷启动” 时间会更长,但是二次启动能够节省很多时间.

Vue-Cli 已经内置了 cache-loader 进行以下两个的缓存了

  • babel-loader 的 cacheDirectory 标志
  • vue-loader 的 cacheDirectory 标志

所以

Vue Cli没有必要添加HardSourceWebpackPlugin

HardSourceWebpackPlugin

注意

HardSourceWebpackPlugin 和 speed-measure-webpack-plugin 不能一起使用

详细说明

https://www.npmjs.com/package/hard-source-webpack-plugin

在启动项目时会针对项目生成缓存,若是项目无package或其他变化,下次就不用花费时间重新构建,直接复用缓存。

安装

1
npm install --save-dev hard-source-webpack-plugin

配置vue.config.js

为模块提供中间缓存,缓存路径是:node_modules/.cache/hard-source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
module.exports = {
configureWebpack: config => {
config.plugin.push(
// 为模块提供中间缓存,缓存路径是:node_modules/.cache/hard-source
new HardSourceWebpackPlugin({
root: process.cwd(),
directories: [],
environmentHash: {
root: process.cwd(),
directories: [],
files: ['package.json', 'yarn.lock']
}
})
// 配置了files的主要原因是解决配置更新,cache不生效了的问题,配置后有包的变化,plugin会重新构建一部分cache
)
}
}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
module.exports = {
configureWebpack: {
plugins:[
new HardSourceWebpackPlugin({
root: process.cwd(),
directories: [],
environmentHash: {
root: process.cwd(),
directories: [],
files: ['package.json', 'yarn.lock']
}
})
]
}
}

更多配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 缓存 加速二次构建速度
new HardSourceWebpackPlugin({
cacheDirectory: "../node_modules/.cache/hard-source/[confighash]",
recordsPath:
"../node_modules/.cache/hard-source/[confighash]/records.json",
configHash: function(webpackConfig) {
return require("node-object-hash")({ sort: false }).hash(
webpackConfig
);
},
// Either false, a string, an object, or a project hashing function.
environmentHash: {
root: process.cwd(),
directories: [],
files: ["../package-lock.json", "../yarn.lock"]
},
// An object.
info: {
// 'none' or 'test'.
mode: "none",
// 'debug', 'log', 'info', 'warn', or 'error'.
level: "debug"
},
// Clean up large, old caches automatically.
cachePrune: {
// Caches younger than `maxAge` are not considered for deletion. They must
// be at least this (default: 2 days) old in milliseconds.
maxAge: 2 * 24 * 60 * 60 * 1000,
// All caches together must be larger than `sizeThreshold` before any
// caches will be deleted. Together they must be at least this
// (default: 50 MB) big in bytes.
sizeThreshold: 100 * 1024 * 1024
}
})

缩小文件检索解析范围

为避免无用的检索与递归遍历,可以使用alias指定引用时候的模块,noParse,对不依赖本地代码的第三方依赖不进行解析。

Vue-Cli默认已进行了如下配置

1
noParse: /^(vue|vue-router|vuex|vuex-router-sync)$/

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义getAliasPath方法,把相对路径转换成绝对路径
const getAliasPath = dir => join(__dirname, dir)
module.exports = {
configureWebpack: config => {
config.module.noParse = /^(vue|vue-router|vuex|vuex-router-sync|lodash|echarts|axios|view-design)$/
}
chainWebpack: config => {
// 添加别名
config.resolve.alias
.set('@', getAliasPath('src'))
.set('assets', getAliasPath('src/assets'))
.set('utils', getAliasPath('src/utils'))
.set('views', getAliasPath('src/views'))
.set('components', getAliasPath('src/components'))
}
// 生产环境禁用eslint
lintOnSave: !process.env.NODE_ENV !== 'production',
}

或者

1
2
3
4
5
6
7
8
// vue.config.js
module.exports = {
configureWebpack:{
module: {
noParse: /^(vue|vue-router|vuex|vuex-router-sync|lodash|echarts|axios|view-design)$/
}
}
}

import优化

运用这个插件能在代码使用了import语法的情况下,大大提高代码的编译速度。

安装 babel-plugin-dynamic-import-node

1
npm install --save-dev babel-plugin-dynamic-import-node

vue-cli3

修改babel.config.js文件

1
2
3
4
5
6
7
8
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
env: {
development: {
plugins: ["dynamic-import-node"]
}
}
};

vue.cli2

.babelrc文件

1
2
3
4
5
6
7
8
9
"env": {
"test": {
"plugins": []
},
"development":{
"presets": ["env", "stage-2"],
"plugins": ["dynamic-import-node"]
}
}

打包体积优化

不生成SourceMap

production环境不生成SourceMap

1
2
3
4
module.exports = {
lintOnSave: false,
productionSourceMap: process.env.NODE_ENV !== "production", //打包不生成map文件
}

图片压缩

image-webpack-plugin

对图片像素要求没很极致的,这个压缩还是可以使用的,压缩率肉眼看起来感觉是没太大区别。 这里注意一下,我没有对svg进行压缩,原因是压缩的svg,再通过构建时被打包成base64时,生成的base64会有问题,无法访问。

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
chainWebpack: config => {
// 对图片进行压缩
config.module
.rule('images')
.test(/\.(png|jpe?g|gif)(\?.*)?$/)
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({ bypassOnDebug: true })
.end()
}
}

gzip压缩

使用 gzip 压缩代码,效果显著。

安装

1
npm install compression-webpack-plugin

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const CompressionWebpackPlugin = require('compression-webpack-plugin')

module.exports = {
configureWebpack: config => {
// 生产环境下生效
if (process.env.NODE_ENV === 'production') {
// 配置 gzip 压缩
config.plugins.push(
new CompressionWebpackPlugin({
test: /\.js$|\.html$|\.css$/,
threshold: 4096 // 超过4kb压缩
})
)
}
}
}

配置CDN

老实说,我不用这个功能的,线上使用 cdn 总让我有一种不安全感,除非公司有自己的 cdn 库,不过这确实也是一种优化方案,效果也还不错。它的配置也很简单,在 externals 中配置,例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
// 配置 cdn,这里将 vue,vue-router 和 axios 三个包配置成 cdn 引入
// 其中 Vue,VueRouter 等名称是该库暴露在全局中的变量名
config.externals = {
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios'
}
}
}
}

然后在 public/index.html 模板文件中引入 cdn 地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title></title>
<!-- 引入 cdn 地址 -->
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.5.10/vue.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.18.0/axios.min.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

我这里使用的是 bootcdn 的地址,需要注意版本问题。

也可以借助 HtmlWebpackPlugin 插件来方便插入 cdn 的引入。

使用 cdn 引入的方式虽然能极大改善网页加载速度,但我还是不会用这个功能,项目还不需要非得这样的优化,也怕 cdn 不稳定。

当然

也可以不用CDN,直接把JS复制到项目下,用相对路径引用即可。

借助 HtmlWebpackPlugin 插件来方便插入 cdn 的引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//生产环境标记
const IS_PRODUCTION = process.env.NODE_ENV === "production";
const path = require("path");
// 生产配置
const cdn_production = {
js: ["/librarys/vue@2.6.11/vue.min.js"]
};
// 开发配置
const cdn_development = {
js: ["/librarys/vue@2.6.11/vue.js"]
};

module.exports = {
configureWebpack: {
externals: {
vue: "Vue",
},
},
chainWebpack: config => {
config.plugin("html").tap(args => {
args[0].cdn = IS_PRODUCTION ? cdn_production : cdn_development;
return args;
});
}
};

index.html中添加

1
2
3
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>

Vue Cli配置说明

https://cli.vuejs.org/zh/guide/webpack.html

简单的配置方式

调整 webpack 配置最简单的方式就是在 vue.config.js 中的 configureWebpack 选项提供一个对象:

1
2
3
4
5
6
7
8
// vue.config.js
module.exports = {
configureWebpack: {
plugins: [
new MyAwesomeWebpackPlugin()
]
}
}

该对象将会被 webpack-merge 合并入最终的 webpack 配置。

链式操作 (高级)

Vue CLI 内部的 webpack 配置是通过 webpack-chain 维护的。这个库提供了一个 webpack 原始配置的上层抽象,使其可以定义具名的 loader 规则和具名插件,并有机会在后期进入这些规则并对它们的选项进行修改。

它允许我们更细粒度的控制其内部配置。接下来有一些常见的在 vue.config.js 中的 chainWebpack 修改的例子。

提示

当你打算链式访问特定的 loader 时,vue inspect 会非常有帮助。

修改 Loader 选项

1
2
3
4
5
6
7
8
9
10
11
12
13
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(
options => {
// 修改它的选项...
return options
})
}
}

提示

对于 CSS 相关 loader 来说,我们推荐使用 css.loaderOptions 而不是直接链式指定 loader。这是因为每种 CSS 文件类型都有多个规则,而 css.loaderOptions 可以确保你通过一个地方影响所有的规则。

externals详解

externals 配置提供了不从 bundle 中引用依赖的方式。

简单理解就是不通过npm下载的类库,在html文件中以script引入,然后在页面中使用import导入的这种方式

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
configureWebpack: config => {
// 配置 cdn,这里将 vue,vue-router 和 axios 三个包配置成 cdn 引入
// 其中 Vue,VueRouter 等名称是该库暴露在全局中的变量名
config.externals = {
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios',
html2canvas: "html2canvas"
}
}
}

如上配置 我们暴漏了html2canvas变量,那么代码中的

1
import html2canvas from "html2canvas";

依旧能正常运行,如果不配置这个我们代码中就只能这样写了

1
const html2canvas = window.html2canvas;

也就是说我们如上配置就可以

  1. 完全不变动之前代码中的引用。
  2. 不建议删除npm引用的包,在npm install的时候可能会安装其他版本的包。

升级Vue-Cli

其实上面的优化效果都不明显,升级Vue-Cli的效果是最好的。

查看Vue-Cli版本

1
vue -V

升级电脑的Vue-Cli版本

1
2
npm uninstall vue-cli -g
npm install -g @vue/cli

因为vue-cli 5已经使用上webpack5,之前vue.config.js文件的一些webpack的配置是有一些调整的。

升级项目中的Vue-Cli版本

1
npm ls core-js

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"name": "myproject",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"report": "vue-cli-service build --report"
},
"dependencies": {
"core-js": "~3.8.3",
"vue": "~2.6.11",
"vue-router": "~3.5.1",
"vuex": "~3.6.2",
"uuid": "~3.3.3"
},
"devDependencies": {
"@babel/core": "~7.12.16",
"@babel/eslint-parser": "~7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "~7.32.0",
"eslint-config-prettier": "~8.3.0",
"eslint-plugin-prettier": "~4.0.0",
"eslint-plugin-vue": "~8.0.3",
"less": "~4.0.0",
"less-loader": "~8.0.0",
"vue-template-compiler": "~2.6.11",
"prettier": "~2.4.1",
}
}

注意

devDependencies中的都替换就行。

vue-template-compiler要和vue的版本一致。

core-js要用3.x版本。

要添加uuid,新版本的vue-cli中默认是没有的。

vue.config.js

1
2
3
4
5
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
//原配置
});

babel.config.js

该文件无变动。

1
2
3
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

.eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
root: true,
env: {
node: true,
},
extends: [
"plugin:vue/essential",
"eslint:recommended",
"plugin:prettier/recommended",
],
parserOptions: {
parser: "@babel/eslint-parser",
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
};

jsconfig.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}