最近一个小伙伴问我他们公司的Vue后台项目怎么首次加载要十多秒太慢了,有什么能优化的,因而乎我打开了他们的网站,发现主要耗时在加载vendor.js文件这个文件高达2M,因而乎我就拿来他们的代码看看,进行了一番折腾。最终仍是取得了不错的效果。javascript
对于网页性能,如何提高加载速度、等原理以及操做,在 修言 大佬 这本 《前端性能优化原理与实践》 书中介绍的很详细,有兴趣的小伙伴能够去看看。css
本文将主要从
webpack
打包的角度进行一些首屏加载速度的优化,以及打包速度的优化的实践html
我选取的是一个用vue-cli2.0+版本构建的 Vue
+ Vuex
+ Vue-router
+ axios
+ elment-ui
的一个后台系统项目进行测试,大概有20个异步加载路由页面。前端
咱们将优化分红了3个主要的角度,每个角度优化后进行速度打包速度的测试,打包构建花费的时间列在下面:vue
优化resolve.modules、配置装载机的 include & exclude、使用webpack-parallel-uglify-plugin 压缩代码java
配置 externals 使库文件采用cdn加载node
webpack DllPlugin、webpack DllReferencePlugin 分离框架库文件webpack
次数\打包耗时(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左右效果。具体时间,固然跟你的项目又关系。接下来,咱们将介绍如何具体操做。ios
咱们首先经过修改基本的
webpack
配置的方式提高打包速率git
原理:
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')
],
...
},
复制代码
原理:
webpack
的 loaders
里的每一个子项均可以有 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'
复制代码
原理:
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
}
}
}),
...
复制代码
原理:
操做:
这一步具体操做,就没贴代码了,我感受没做用不明显,时间还加了一点点,多是跟项目有关把,想使用的小伙伴自行百度用到本身项目里面试试。
当你把上面这些优化都作完了,运行build的时候发现第一次所须要的构建时间跟最开始同样23s左右,稍微少了2秒(主要是优化resolve,loader等的效果)
再次build的时候时间大大减小,由于在跟目录下 .cache/
下缓存了 Uglify
相关的js多以大大提升了构建的速度。赶忙去试试把。小伙伴们。
开头说到因为
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
中添加如下内容<!DOCTYPE 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>
<!-- built files will be auto injected -->
</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: '<App/>',
components: { App }
})
复制代码
完事
上面都配置好了后启动 npm run build
发现构建时间在11-12s左右,为何相比较于步骤1的提高并不大呢,由于步骤1中 ParallelUglifyPlugin
在重复构建中,并无改动代码,缓存起了重要做用
vendor
包Parsed 后只有
24KB
左右,框架文件利用cdn加速,以及浏览器缓存机制,能够显著提高首页的访问速度。咱们能够把文件部署在服务器上,打开Chrome network查看具体的加载用时。
缺点
vue-devtools
谷歌调试工具了,毕竟直接用的线上的资源包。可是,根据环境作区分修改部分代码,就能够实现开发环境用的本地包,打包后的使用cdn资源。具体请参考这位大佬的实践 Vue SPA 首屏加载优化实践 ,能够区分环境来引入。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 Bundleconst 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)