最近一个小伙伴问我他们公司的Vue后台项目怎么首次加载要十多秒太慢了,有什么能优化的,于是乎我打开了他们的网站,发现主要耗时在加载vendor.js文件这个文件高达2M,于是乎我就拿来他们的代码看看,进行了一番折腾。最终还是取得了不错的效果。
优化思路
对于网页性能,如何提升加载速度、等原理以及操作,在 修言 大佬 这本 《前端性能优化原理与实践》 书中介绍的很详细,有兴趣的小伙伴可以去看看。
本文将主要从
webpack
打包的角度进行一些首屏加载速度的优化,以及打包速度的优化的实践
优化成效
我选取的是一个用vue-cli2.0+版本构建的 Vue
+ Vuex
+ Vue-router
+ axios
+ elment-ui
的一个后台系统项目进行测试,大概有20个异步加载路由页面。
我们将优化分成了3个主要的角度,每一个角度优化后进行速度打包速度的测试,打包构建花费的时间列在下面:
-
优化resolve.modules、配置装载机的 include & exclude、使用webpack-parallel-uglify-plugin 压缩代码
-
配置 externals 使库文件采用cdn加载
-
webpack DllPlugin、webpack DllReferencePlugin 分离框架库文件
次数\打包耗时(s) | 原始配置用时 | 优化步骤1 | 优化步骤2 | 优化步骤3 |
---|---|---|---|---|
1 | 24.86 | ==23.86== | 11.22 | 13.92 |
2 | 23.52 | 14.51 | 11.04 | 12.63 |
3 | 25.49 | 14.04 | 11.29 | 13.19 |
4 | 24.84 | 14.56 | 11.25 | 13.14 |
5 | 24.60 | 15.44 | 11.86 | 14 |
由此可看出,还是能达到显著的提升了10多s左右效果。具体时间,当然跟你的项目又关系。接下来,我们将介绍如何具体操作。
优化步骤
1. 通过基本的webpack插件来加速打包
我们首先通过修改基本的
webpack
配置的方式提升打包速率
1.优化resolve.modules
原理:
-
webpack 的 resolve.modules 是用来配置模块库(即 node_modules)所在的位置。当 js 里出现 import 'vue' 这样不是相对、也不是绝对路径的写法时,它便会到 node_modules 目录下去找。
-
在默认配置下,webpack 会采用向上递归搜索的方式去寻找。但通常项目目录里只有一个 node_modules,且是在项目根目录。为了减少搜索范围,可我们以直接写明 node_modules 的全路径
所以平时在写 import
导入模块的时候引入指向的是具体的哪个文件,也对打包速度的提升又一定的影响
操作:
打开 build/webpack.base.conf.js
文件,添加如下 modules
代码块:
module.exports = {
resolve: {
...
modules: [
resolve('src'),
resolve('node_modules')
],
...
},
复制代码
2.配置loader的 include & exclude
原理:
webpack
的loaders
里的每个子项都可以有 include 和 exclude 属性:
- include:导入的文件将由加载程序转换的路径或文件数组(把要处理的目录包括进来)
- exclude:不能满足的条件(排除不处理的目录)
- 我们可以使用 include 更精确地指定要处理的目录,这可以减少不必要的遍历,从而减少性能损失。
- 同时使用 exclude 对于已经明确知道的,不需要处理的目录,予以排除,从而进一步提升性能。
操作:
打开 build/webpack.base.conf.js
文件,添加如下 include
,exclude
配置:
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig,
include: [resolve('src')], // 添加配置
exclude: /node_modules\/(?!(autotrack|dom-utils))|vendor\.dll\.js/ // 添加配置
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')], // 添加配置
exclude: /node_modules/ // 添加配置
},
复制代码
除此之外,如果我们选择开启缓存将转译结果缓存至文件系统,则至少可以将 babel-loader 的工作效率提升两倍。要做到这点,我们只需要为 loader 增加相应的参数设定:
loader: 'babel-loader?cacheDirectory=true'
复制代码
3.使用 webpack-parallel-uglify-plugin 插件来压缩代码
原理:
- 默认情况下
webpack
使用UglifyJS
插件进行代码压缩,但由于其采用单线程压缩,速度很慢。 - 我们可以改用
webpack-parallel-uglify-plugin
插件,它可以并行运行UglifyJS
插件,从而更加充分、合理的使用 CPU 资源,从而大大减少构建时间,该插件能设置缓存,大大减小构建时间。
操作: 1.安装 webpack-parallel-uglify-plugin
插件
yarn add webpack-parallel-uglify-plugin -D
// or
npm i webpack-parallel-uglify-plugin -D
复制代码
2.打开 build/webpack.prod.conf.js
文件,并作如下修改
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
...
// 删掉webpack提供的UglifyJS插件
//new UglifyJsPlugin({
// uglifyOptions: {
// compress: {
// warnings: false
// }
// },
// sourceMap: config.build.productionSourceMap,
// parallel: true
//}),
// 增加 webpack-parallel-uglify-plugin来替换
new ParallelUglifyPlugin({
cacheDir: '.cache/',
uglifyJS:{
output: {
comments: false
},
compress: {
warnings: false,
drop_debugger: true, // 去除生产环境的 debugger 和 console.log
drop_console: true
}
}
}),
...
复制代码
使用 HappyPack 来加速代码构建
原理:
- 由于运行在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack 需要处理的事情只能一件一件地做,不能多件事一起做。
- 而 HappyPack 的处理思路是:将原有的 webpack 对 loader 的执行过程,从单一进程的形式扩展多进程模式,从而加速代码构建。
操作:
这一步具体操作,就没贴代码了,我感觉没作用不明显,时间还加了一点点,可能是跟项目有关把,想使用的小伙伴自行百度用到自己项目里面试试。
查看效果
当你把上面这些优化都做完了,运行build的时候发现第一次所需要的构建时间跟最开始一样23s左右,稍微少了2秒(主要是优化resolve,loader等的效果)
再次build的时候时间大大减少,因为在跟目录下 .cache/
下缓存了 Uglify
相关的js多以大大提高了构建的速度。赶紧去试试把。小伙伴们。
2. 配置 externals 使库文件采用cdn加载
开头说到由于
vendor.js
过大引起的首页加载慢,但是vue打包好的 vendor.js 是由什么构成的呢?
vue-cli 生成的项目中 集成了 webpack-bundle-analyzer 依赖可视化分析工具
运行
npm run build --report
复制代码
根据上图所知 vendor.js
Parsed 后为739kb,包主要包含了 像 Vue
、 Vue-router
、 elment-ui
等之类需要全局引入的库文件。这些库文件都是一些不经常变动的问题,所以我们可以考虑把他们分离出来,用cdn的方式把框架库引入。
原理:
利用 webpack
的 externals
属性 。文档
官网的解释 :防止 将某些 import 的包(package) 打包 到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
通俗的解释:让某些资源包即使不在本地npm安装,通过 script
标签引入后也能使用
操作:
- 首先在模板文件
index.html
中添加以下内容
<html>
<head>
<meta charset="utf-8">
<title>XXXX平台title>
<link rel="stylesheet" href="https://cdn.bootcss.com/element-ui/2.4.1/theme-chalk/index.css">
head>
<body>
<div id="app">div>
<script src="https://cdn.bootcss.com/vue/2.5.2/vue.min.js">script>
<script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js">script>
<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js">script>
<script src="https://cdn.bootcss.com/axios/0.17.0/axios.min.js">script>
<script src="https://cdn.bootcss.com/element-ui/2.4.1/index.js">script>
body>
html>
复制代码
注意!版本号要与 package.json
中的版本号一致
- 修改
build/webpack.base.conf.js
module.exports = {
...
externals: {
'vue': 'Vue',
'vuex': 'Vuex',
'vue-router': 'VueRouter',
'axios': 'axios',
'element-ui': 'ELEMENT'
}
...
}
复制代码
注意!这里 axios
变量名要使用 axios
注意!这里 element-ui
变量名要使用 ELEMENT
,因为element-ui
的 umd
模块名是 ELEMENT
- 修改
src/router/index.js
// import Vue from 'vue'
import VueRouter from 'vue-router'
// 注释掉
// Vue.use(VueRouter)
...
}
复制代码
- 修改
src/store/index.js
...
// 注释掉
// Vue.use(Vuex)
...
}
复制代码
- 修改
src/main.js
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui'
// 注释掉
// import 'element-ui/lib/theme-chalk/index.css'
// router setup
import router from './router'
// Vuex setup
import store from './store'
Vue.use(ElementUI)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
template: ' ',
components: { App }
})
复制代码
完事
上面都配置好了后启动 npm run build
发现构建时间在11-12s左右,为什么相比较于步骤1的提升并不大呢,因为步骤1中 ParallelUglifyPlugin
在重复构建中,并没有改动代码,缓存起了重要作用
vendor
包Parsed 后只有 24KB
左右,框架文件利用cdn加速,以及浏览器缓存机制,可以显著提升首页的访问速度。我们可以把文件部署在服务器上,打开Chrome network查看具体的加载用时。
缺点
- 此方法就没办法使用
vue-devtools
谷歌调试工具了,毕竟直接用的线上的资源包。但是,根据环境做区分修改部分代码,就可以实现开发环境用的本地包,打包后的使用cdn资源。具体请参考这位大佬的实践 Vue SPA 首屏加载优化实践 ,可以区分环境来引入。 - 请求代价可能大于下载代价,在web优化指南中,就是尽量整合文件,减小请求数量,这样多了很多cdn资源并不一定合适。。
3.webpack DllPlugin
、webpack DllReferencePlugin
预编译第三方库文件
既然 cdn 还是有他的弊端,那么我们为何不考虑把库文件合并呢,所以我们利用
webpack.DllPlugin
+webpack DllReferencePlugin
+add-asset-html-webpack-plugin
预编译并且引入
原理:
- 利用
webpack DllPlugin
插件将第三方插件单独打包出来至vendor.dll.js
- 利用
webpack DllReferencePlugin
是把这些预先编译好的模块引用起来 - 利用
add-asset-html-webpack-plugin
把vendor.dll.js
包插入html
操作:
我们还是从操作1完成后继续修改代码(cdn的相关操作代码退回)
- 在
build
文件夹中新建webpack.dll.conf.js
文件,内容如下(主要是配置下需要提前编译打包的库):
var path = require('path')
var webpack = require('webpack')
var context = path.join(__dirname, '..')
module.exports = {
entry: {
vendor: [
'vue/dist/vue.common.js',
'vuex',
'vue-router',
'axios',
'element-ui'
]
},
output: {
path: path.join(context, 'static/js'), // 打包后的 vendor.js放入 static/js 路径下
filename: '[name].dll.js',
library: '[name]'
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
plugins: [
new webpack.DllPlugin({
path: path.join(context, '[name].manifest.json'),
name: '[name]',
context: context
}),
// 压缩js代码
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
output: { // 删除打包后的注释
comments: false
}
})
]
}
复制代码
- 编辑
package.json
文件,添加一条编译命令:
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"lint": "eslint --ext .js,.vue src",
"build": "node build/build.js",
"build:dll": "webpack --config build/webpack.dll.conf.js --progress"
},
复制代码
然后命令行运行 npm run build:dll
这时,会在 static/js 里面生成 vendor.dll.js
, vendor
属性内的相关库文件就打包在内了。
- 打开
index.html
这边将vendor.dll.js
引入进来。
<body>
<div id="app">div>
<script src="./static/js/vendor.dll.js">script>
body>
复制代码
- 打开
build/webpack.base.conf.js
文件,编辑添加如下配置,作用是通过 DLLReferencePlugin 来使用 DllPlugin 生成的 DLL Bundle
const webpack = require('webpack');
module.exports = {
...
plugins: [
new webpack.DllReferencePlugin({
// name参数和dllplugin里面name一致,可以不传
name: 'vendor',
// dllplugin 打包输出的manifest.json
manifest: require('../vendor.manifest.json'),
// 和dllplugin里面的context一致
context: path.join(__dirname, '..')
})
]
...
}
复制代码
- 修改
build/webpack.prod.js
注释掉CommonsChunkPlugin
相关代码,因为库文件在之前的 vendor.dll.js 中已经编译好了,不需要在编译
module.exports = {
plugins: [
...
// 去掉这里的CommonsChunkPlugin
// new webpack.optimize.CommonsChunkPlugin({
// name: 'vendor',
// minChunks (module) {
// // any required modules inside node_modules are extracted to vendor
// return (
// module.resource &&
// /\.js$/.test(module.resource) &&
// module.resource.indexOf(
// path.join(__dirname, '../node_modules')
// ) === 0
// )
// }
// }),
// 去掉这里的CommonsChunkPlugin
// new webpack.optimize.CommonsChunkPlugin({
// name: 'manifest',
// minChunks: Infinity
// }),
...
]
}
复制代码
完事
至此,保存代码,进行构建,发现构建时间大概在14s左右。怎么比cdn时间还增多了呢,因为element-ui的样式文件还需要每次打包,样式不建议单独打包出来,要么也是使用cdn的方式。
最后我们还是部署到服务器上打开Chrome network查看网页具体的加载用时。
打开构建依赖图,发现vendor
文件已经不见了,不需要每次打包了,直接引入 vendor.dll.js
文件就好,这样还有一个好处:当你有多个项目的依赖相同的时候,引用同一份 dll
即可。
真的就完事儿了? 大家有没有注意到 vendor.dll.js
是一个固定的文件,没有加 hash 后缀,这对缓存来说是致命的,当你升级了库或者增加了库文件,重新打包后的 还是叫做 vendor.dll.js
文件,没有破坏缓存,当用户访问时程序可能会出现问题。
有时候开发环境和测试环境可能 引入的vendor.dll.js
路径不一样你得手动更改,也是一个问题。既然这样怎么办呢??
还好有 add-asset-html-webpack-plugin
这个插件进行依赖资源的注入,本人在实践的时候以为找到了救命稻草。可是奈何不知道是姿势不对,还是该插件已经过时未升级,程序运行时候报错,无法使用,也希望使用过的大佬,指点一下。。
结语
至此关于 Vue SPA 项目中的优化,介绍的差不多了,但是仅仅只是提供一个思路,优化并不是一成不变的,有些项目可能只需要步骤1,有些项目可能引用资源小采用cdn的方式也可以,而有些多个项目依赖都相同,就可考虑dll,当然是根据具体的场景来进行选择优化。
最终还是以部署到服务器后,清除缓存访问,后分析加载时间。毕竟加载时间比打包时间重要得多
但是,我们平时写代码的时候应该多多思考,在写代码的时候注意一些细节,也能提升不少效率和性能。
举个例子1:很多项目会用到 echarts
,我发现有小伙伴把 echarts
注入在 main.js
中,这显然是没必要的白白增大了 vendor.js
的大小,应该在仅仅需要使用的页面去引入就好,还得注意echarts
的地图组件,是采用同步渲染,还是异步渲染好呢,还有根据窗口的 resize
,是否注意防抖和节流呢。
举个例子2:当我们使用百度地图的jssdk的时候,是在 index.html
里面通过 script
标签引入,还是在某个页面需要使用地图的时候采用异步加载的形式呢。这些都是值得我们思考的问题。
所以从每一步写代码的细节多多思考。
至此写完了,我也是抱着学习的态度,如有什么错误,请大佬们斧正,顺便请教 add-asset-html-webpack-plugin
的正确姿势。
附录
相关代码托管在github vue-spa-optimization 上,上面有4个分支
master:
:未做任何优化的原始版本simple:
做了上面步骤1中相关优化的版本cdn:
做了上面步骤1与步骤2优化的版本(cdn)dll:
做了上面步骤1与步骤3优化的版本(dll)