前言
之前的开发工作中对vue2项目也有做过不同的优化,但是都比较零散且没有系统化的记录下来。
正好最近接手了一个老项目,虽然项目不大性能也还过得去,但是我觉得可优化的空间还是比较多的。
所以也趁此机会能把vue打包,编译,启动时间的一些优化方法记录下。在阅读之前你需要确保以下几点:
本项目是基于vue2.x + vue-cli4.x的,不适用于vue3.x+vite项目
本文一些配置的写法可能因插件或者loader版本差异不同而报错,如出现错误,请查阅官方文档最新配置信息
文章不会一次性把全部配置代码贴出来,目的是为了让读者们能够真正熟悉每一项优化配置
文章介绍了多种优化方式,希望读者不要全部照抄,而是应该根据自己项目和实际情况进行选择配置。没有最完美的配置,只有最适合自己的配置~
好了,闲话不多说了。在开始之前我们先看下打包体积大小和时间,这样有利于我们优化后进行对比。
优化前:
1. 打包体积:6.38M
2. 冷启动耗费时间:61.39秒
3. 编译耗费时间:50.13秒
准备工作
1. 安装speed-measure-webpack-plugin
ini
npm i speed-measure-webpack-plugin
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
module.exports = {
configureWebpack: {
plugins: [
new BundleAnalyzerPlugin(),
new SpeedMeasurePlugin(),
]
}
};
2. 安装webpack-bundle-analyzer
ini
+ npm i webpack-bundle-analyzer
+ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
configureWebpack: {
plugins: [
+ new BundleAnalyzerPlugin(),
new SpeedMeasurePlugin(),
]
}
};
一、减少打包代码体积
1. 使用externals和CDN
-
externals
允许我们在编译过程中排除那些已经在运行时环境存在的全局变量或CDN加载的库,而不是将它们包含在最终的bundle文件内。 -
当配置了
externals
字段后,Webpack会在生成的bundle中引用这些全局变量,而不是把它们实际打包进去。这样做的好处有:(1)减小bundle体积:如果你的应用依赖了一些大型第三方库(如jQuery、Vue自身、React、大型UI框架等),并且你确定用户在访问你的应用时已经通过其他方式(比如在HTML头部引入CDN链接)加载了这些库,那么就没有必要在每个用户的请求中重复加载这些库。
(2)加快页面加载速度:由于第三方库直接从CDN加载,通常情况下CDN会提供更快的加载速度和更好的缓存机制。
-
externals缺点:直接在html内引入的,失去了按需引入的功能,只能引入组件库完整的js和css
csharp
// vue.config.js
configureWebpack: {
// provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title.
name,
resolve: {
alias: {
'@': resolve('src'),
},
},
externals: {
axios: 'axios',
'vue-router': 'VueRouter',
'vue': 'Vue',
'Vuex': 'Vuex',
'element-ui': 'ELEMENT',
}
}
xml
// index.html
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.18.1/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14"></script>
<script src="https://unpkg.com/vuex@3.1.0/dist/vuex.global.js"></script>
<script src="https://unpkg.com/vue-router@3.0.6/dist/vue-router.js"></script>
<script src="https://unpkg.com/element-ui@2.15.12/lib/index.js"></script>
2. 图片压缩
2.1 使用压缩网站
2.2 使用image-webpack-loader
arduino
npm i image-webpack-loader
chainWebpack: config => {
config.module
.rule('images')
.test(/.(png|jpe?g|gif|svg)(?.*)?$/)
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({
bypassOnDebug: true })
.end()
},
在安装image-webpack-loader会遇到一个大坑就是安装失败,如果安装或者使用过程中遇到错误请尝试以下步骤解决
1.删除: npm uninstall image-webpack-loader
2.使用cnpm重新安装:cnpm install image-webpack-loader
2.3 两种方式对比
压缩网站:一个从600KB 压缩到了144KB ,一个从98KB 压缩到了18KB
image-webpack-loader插件: 一个从600KB 压缩到了148KB ,一个从98KB 压缩到了10KB
3.启用gzip压缩
-
在Vue.js项目中开启Gzip压缩是一种常见的性能优化手段,它能够显著减小静态资源(如JavaScript、CSS和HTML文件)在网络传输过程中的大小,从而提高页面加载速度。
-
线上的项目,一般都会结合构建工具 webpack 插件或服务端配置 nginx,来实现 http 传输的 gzip 压缩,目的就是把服务端响应文件的体积尽量减小,优化返回速度。html、js、css资源,使用 gzip 后通常可以将体积压缩70%以上
javascript
const CompressionWebpackPlugin = require("compression-webpack-plugin");
chainWebpack: config => {
// 生产环境,开启js\css压缩
if (process.env.NODE_ENV === "production") {
config.plugin("compressionPlugin").use(
new CompressionWebpackPlugin({
test: /.(js|css|less)$/, // 匹配文件名
threshold: 10240, // 对超过10k的数据压缩
minRatio: 0.8,
deleteOriginalAssets: true // 删除源文件
})
);
}
}
- 服务器配置: Gzip压缩后的文件需要服务器支持并正确返回给客户端。对于Nginx服务器,你需要在Nginx的配置文件中添加如下内容以启用Gzip:
bash
//nginx.conf
http {
gzip on;
gzip_vary on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
4.uglyifyjs
打包后的 JavaScript 代码进行混淆压缩,从而减少文件大小以优化网页加载速度。这个插件基于 UglifyJS 库,可以执行多种优化操作,包括但不限于删除无用代码、简化变量名、内联函数调用等。
php
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
config.plugins.push(
new UglifyJsPlugin({
uglifyOptions: {
compress: {
drop_debugger: true,//生产环境自动删除debugger
drop_console: true, //生产环境自动删除console
},
warnings: false,
},
sourceMap: false, //关掉sourcemap 会生成对于调试的完整的.map文件,但同时也会减慢打包速度
parallel: true, //使用多进程并行运行来提高构建速度。默认并发运行数:os.cpus().length - 1。
}),
5.路由懒加载
Vue.js 中的路由懒加载是一种优化技术,它允许我们在用户导航到特定路由时异步加载对应的组件,而不是在应用程序启动时一次性加载所有组件。这样可以显著减少初始加载时间、减小打包后的文件体积,并提升用户体验。
javascript
// 使用 `import()` 动态导入
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{ path: '/about', component: () => import('@/views/About.vue'), // 懒加载 About 组件 },
{ path: '/contact', component: () => import(/* webpackChunkName: "contact" */ '@/views/Contact.vue'), // 命名代码块
},
],
})
javascript
// 使用异步组件声明
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [ {
path: '/example',
component: {
// 使用异步组件定义
async component() {
const { default: ExampleComponent } = await import('@/views/Example.vue');
return ExampleComponent;
},
},
},
// 其他路由...
],
})
这两种方式都可以确保当用户首次访问相应路由时,Vue Router 才会去加载对应的组件资源。这样不仅提高了应用的响应速度,也降低了初次加载时对网络带宽的需求。
6. preload预加载
浏览器自己提供的一种机制,在页面加载的时候,预先请求资源,一边在需要的时候可以更快的获取这些资源。
也就是设置文件中的黑名单,配置文件,添加一个数组,用来储存黑名
css
npm install --save-dev script-ext-html-webpack-plugin
const blackList = ['script1.js', 'script2.js'];
plugins: [
new ScriptExtHtmlWebpackPlugin({
preload: {
test: /.js$/,
excludeChunks: blackList
}
})
]
// 删除文件中各个脚本都在使用的脚本命令,然后统一在html页面中进行封装,需要用到的时候,去html页面中进行获取,这样就可以减少http的请求
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{ // `runtime` must same as runtimeChunk name. default is `runtime` inline: /runtime\..*\.js$/, }, ])
.end();
7. DllPlugin 动态链接库
DllPlugin
与 externals 的作用相似,都是将依赖抽离出去,节约打包时间。区别是 DllPlugin 是将依赖单独打包,这样以后每次只构建业务代码,而 externals 是将依赖转化为 CDN 的方式引入
当公司没有很好的 CDN 资源或不支持 CDN 时,就可以考虑使用 DllPlugin ,替换掉 externals
css
//1. 创建 dll.config.js 配置文件
import { DllPlugin } from "webpack";
export default {
// 需要抽离的依赖
entry: {
vendor: ["vue", "vue-router", "axios", "echarts"]
},
mode: "production",
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: "all",
name: "vendor",
test: /node_modules/
}
}
}
},
output: {
filename: "[name].dll.js", // 输出路径和文件名称
library: "[name]", // 全局变量名称:其他模块会从此变量上获取里面模块
path: AbsPath("dist/static") // 输出目录路径
},
plugins: [
new DllPlugin({
name: "[name]", // 全局变量名称:减小搜索范围,与output.library结合使用
path: AbsPath("dist/static/[name]-manifest.json") // 输出目录路径
})
]
};
json
// package.json 配置脚本
"build:dll": "webpack --config ./dll.config.js",
javascript
// 使用 `DllReferencePlugin` 将打包生成的dll文件,引用到需要的预编译的依赖上来,并通过 `html-webpack-tags-plugin` 在打包时自动插入dll文件
// vue.config.js 配置如下
import { DllReferencePlugin } from "webpack";
import HtmlTagsPlugin from "html-webpack-tags-plugin";
export default {
configureWebpack: {
plugins: [
new DllReferencePlugin({
manifest: AbsPath("dist/static/vendor-manifest.json") // manifest文件路径
}),
new HtmlTagsPlugin({
append: false, // 在生成资源后插入
publicPath: "/", // 使用公共路径
tags: ["static/vendor.dll.js"] // 资源路径
})
]
}
};
先运行 npm run build:dll
打包生成依赖文件,以后只用运行 npm run build
构建业务代码即可
8. 处理Moment中语言包
一般如果项目引入了moment.js或者引入的某个第三方插件包使用了moment.js, 通常情况下,会全部引入了所有语言包,体积过大,我们需要对其进行处理。
arduino
configureWebpack: config => {
config.plugins.push(new webpack.ContextReplacementPlugin(/moment[/\]locale$/, /zh-cn/))
}
9. 使用dayjs代替momentjs
Day.js 和 Moment.js 是 JavaScript 中两个非常流行的时间日期处理库,它们具有相似的API设计和功能特性,但又存在一些关键的区别:
Moment.js:
- 功能全面:Moment.js 提供了丰富的日期时间操作功能,包括解析、格式化、比较、计算(加减日期)、时区转换等。
- 体积较大:Moment.js 的大小相对较大,尤其是包含所有功能的情况下。在资源优化方面可能存在劣势,特别是在移动应用或对加载速度有严格要求的项目中。
- 国际化支持:Moment.js 提供强大的国际化支持,可以方便地处理不同语言环境下的日期格式和显示问题。
Day.js:
- 轻量级:Day.js 设计初衷是为了提供一个与 Moment.js API 兼容但更轻量的替代方案,其体积比 Moment.js 小得多,有助于减少打包后的文件大小,提高页面加载速度。
- 性能优化:由于其轻量化的设计,Day.js 在性能上通常会优于 Moment.js,尤其是在内存占用和执行效率方面。
- 同样全面的功能:尽管 Day.js 更轻巧,但它仍然提供了大部分 Moment.js 的核心功能,如解析、格式化、比较等操作,并且也支持插件扩展以实现更多功能。
- 国际化支持:同样,Day.js 也具备良好的国际化支持,通过引入相应的语言包即可实现多种语言环境下的日期处理。
总结起来,如果你需要一个功能强大、历史悠久且广泛使用的日期库,Moment.js 是一个很好的选择。然而,如果对代码大小和性能有较高要求,或者只是进行基础的日期时间操作,Day.js 可能更适合你的项目需求。随着技术的发展,越来越多的新项目可能会倾向于使用 Day.js 这种更为现代和轻量的解决方案。
10. splitChunks单独打包第三方包
项目中的第三方库默认会被打包到一个文件名含vendors的bundle中,如果你的项目里面引用的第三方库过多,那么你的vendors就会很臃肿,文件也会变大,网站加载该文件的时候就越耗时,从而影响网站性能。
这个时候我们可以考虑把一些比较大的第一方库从vendors中分离出来,或者直接配置cdn。这里我们主要来讲如何在vue-cli4中单独打包第三方库文件从而实现bundle分割,减小vendors文件体积的目的。
diff
常用参数:
- minSize(默认是30000):形成一个新代码块最小的体积
- minChunks(默认是1):在分割之前,这个代码块最小应该被引用的次数(译注:保证代码块复用性,默认配置的策略是不需要多次引用也可以被分割)
- maxInitialRequests(默认是3):一个入口最大的并行请求数
- maxAsyncRequests(默认是5):按需加载时候最大的并行请求数。
- chunks (默认是async) :initial、async和all
- test: 用于控制哪些模块被这个缓存组匹配到。原封不动传递出去的话,它默认会选择所有的模块。可以传递的值类型:RegExp、String和Function
- name(打包的chunks的名字):字符串或者函数(函数可以根据条件自定义名字)
- priority :缓存组打包的先后优先级。
yaml
module.exports = {
configureWebpack: config => {
if (progress.env.NODE_ENV === 'production') {
config.optimization = {
splitChunks: {
cacheGroups: {
common: {
name: "chunk-common",
chunks: "initial",
minChunks: 2,
maxInitialRequests: 5,
minSize: 0,
priority: 1,
reuseExistingChunk: true,
enforce: true
},
vendors: {
name: "chunk-vendors",
test: /[\\/]node_modules[\\/]/,
chunks: "initial",
priority: 2,
reuseExistingChunk: true,
enforce: true
},
elementUI: {
name: "chunk-hui",
test: /[\\/]node_modules[\\/]hui[\\/]/,
chunks: "all",
priority: 3,
reuseExistingChunk: true,
enforce: true
},
echarts: {
name: "chunk-echarts",
test: /[\\/]node_modules[\\/](vue-)?echarts[\\/]/,
chunks: "all",
priority: 4,
reuseExistingChunk: true,
enforce: true
}
}
}
};
}
},
chainWebpack: config => {
if (progress.env.NODE_ENV === 'production') {
config.optimization.delete("splitChunks");
}
return config;
}
};
11.production环境不生成SourceMap
arduino
module.exports = {
lintOnSave: false,
productionSourceMap: process.env.NODE_ENV !== "production", //打包不生成map文件
}
12. 添加别名 alias
- 通常项目较大的时候我们会引入别名来方便引入,添加别名方法如下:
csharp
module.exports = {
chainWebpack: config => { // 添加别名
config.resolve.alias
.set("vue$", "vue/dist/vue.esm.js")
.set("@", resolve("src"))
.set("@assets", resolve("src/assets"))
.set("@scss", resolve("src/assets/scss"))
.set("@components", resolve("src/components"))
.set("@plugins", resolve("src/plugins"))
.set("@views", resolve("src/views"))
.set("@router", resolve("src/router"))
.set("@store", resolve("src/store"))
.set("@layouts", resolve("src/layouts"))
.set("@static", resolve("src/static"));
}
};
二、编译时间提升
1. 缓存 hard-source-webpack-plugin
javascript
npm i hard-source-webpack-plugin
const HardSourceWebpackPlugin = require("hard-source-webpack-plugin")
plugins: [
new BundleAnalyzerPlugin(),
new SpeedMeasurePlugin(),
new HardSourceWebpackPlugin()
],
//提升第二次运行时间
2. Happywebpack
由于运行在 Node.js 之上的 webpack 是单线程模型的,我们需要 webpack 能同一时间处理多个任务,发挥多核 CPU 电脑的威力
HappyPack
就能实现多线程打包,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,来提升打包速度
ini
npm install HappyPack -D
const HappyPack = require('happypack');
const os = require('os');
// 开辟一个线程池,拿到系统CPU的核数,happypack 将编译工作利用所有线程
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module.exports = {
configureWebpack: {
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader'],
threadPool: happyThreadPool
})
]
}
}
注意:
1.由于测试项目较小,打包时间缩短的不算太多。实测发现越是复杂的项目,HappyPack 对打包速度的提升越明显.
2.由于HappyPack作者已经停止了更新维护,现在更多的是推荐使用 thread-loader
3. thread-loader ------ 开启多线程优化
thread-loader
是官方维护的多进程loader,功能类似于happypack,也是通过开启子任务来并行解析文件,从而提高构建速度。
把这个loader放在其他loader前面。不过该loader是有限制的。示例:
- loader无法发出文件。
- loader不能使用自定义加载器API。
- loader无法访问网页包选项。
每个worker都是一个单独的node.js进程,其开销约为600毫秒。还有进程间通信的开销。在小型项目中使用thread-loader
可能并不能优化项目的构建速度,反而会拖慢构建速度,所以使用该loader时需要明确项目构建构成中真正耗时的过程。
使用 thread-loader
时,通常会将其配置在需要进行大量计算或者时间消耗较大的Loader之前,如Babel这样的转译Loader。通过创建额外的子进程处理这些耗时任务,主线程能够更好地并发执行其他构建步骤,从而提升整体性能。
lua
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module.rule('vue')
.use('thread-loader')
.loader('thread-loader')
.end()
// 在目標 loader 前插入 thread-loader
.use('vue-loader')
.loader('vue-loader')
.end()
}
}