Vue-CLI 性能优化
问题
是否有首屏加载时间过长的问题.?
为什么会出现这样的问题.?
解决方法是什么.?
下面方法都可以大程度的优化项目结构, 进而减少首屏加载的时间
首先得知道 Webpack 的 build 是如何打包分割代码的
目前代码打包分割有三个规则
- 入口起点, 根据
entry
配置来分割代码 - 动态导入: 通过模块引入
import()
分割代码 splitChunks
: 代码分割配置规则, 根据规则分割代码
以下使用vue create
创建一个新项目, build
后输出的文件为以下
Q1: 为什么是打包出这几个文件.?
这里就要查看一下Vue CLI
的默认打包配置了
以下贴出关键代码
{
// 入口配置
entry: {
app: [
'./src/main.js'
]
},
// 出口配置
output: {
path: 'E:\\demo\\vue-cli-vue2-async-components-demo\\dist',
filename: 'js/[name].[contenthash:8].js',
publicPath: '/',
chunkFilename: 'js/[name].[contenthash:8].js'
},
// 代码分割配置
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
}
}
}
在这里可以看到:
清楚知道app.[contenthash:8].js
是由于 规则1 的分割规则, 通过入口配置entry
分割出来的
那么about.[contenthash:8].js
呢?
这时候就要查看Vue Router
的路由懒加载了
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') // 关键代码
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
以上就是初始化项目的路由配置
关键代码component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
webpackChunkName
是什么.?Webpack的魔法注释, 作用: 分割到哪个chunk内
最后的chunk-vendors.[contenthash:8].js
就是因为splitChunks
的分割规则切割出来
主要就是包含node_modules
内引用的插件的包了
代码分割
在Vue项目中, 除了第一个路由页面, 其他路由都是使用 import()
引入, 进行路由懒加载, 当路由被访问的时候才加载对应组件,这样就更加高效了
这其实是利用了Vue 的异步组件 (opens new window)和 Webpack 的代码分割功能 (opens new window),轻松实现路由组件的懒加载
其中有个重要的点, 就是Webpack的魔法注释
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
Webpack 会根据这个魔法注释的名字进行代码分隔, 如上配置之后, 会有一个命名的group-foo
的chunk被单独打包出来一个文件
这个效果不仅仅可应用于路由懒加载, 也可以使用到单文件组件中
组件分割
在父组件中, 需要点击按钮后弹出一个弹窗 一般会把这个弹窗封装为单独一个单文件组件, 一般如下:
直接引入组件, 直接注册到components
中
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<el-button @click="$refs.homeAdd.dialogVisible = true">显示</el-button>
<HelloWorld msg="Welcome to Your Vue.js App" />
<HomeAdd ref="homeAdd"></HomeAdd>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
import HomeAdd from './HomeAdd.vue'
export default {
name: 'Home',
mounted() {
console.log('Home')
},
components: {
HelloWorld,
HomeAdd
}
}
</script>
这样编写代码后的打包是这样的:
能看到app.[contenthash:8].js
从之前的6.57kb
增加到了6.86kb
因为打包把引入的组件也打包进去了, 当页面越来越多, 就会一直增加该主包的体积
其实可以这样写:
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<el-button @click="$refs.homeAdd.dialogVisible = true">按钮</el-button>
<HelloWorld msg="Welcome to Your Vue.js App" />
<HomeAdd ref="homeAdd"></HomeAdd>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'Home',
mounted() {
console.log('Home')
},
components: {
HelloWorld,
HomeAdd: () => import(/* webpackChunkName: "home.add" */ './HomeAdd.vue')
}
}
</script>
通过这样, 可以把弹窗单独打包成home.add
的chunk, 使得该业务分隔成更多更小的包, 避免某个包过大导致的加载时候耗费过多时间
插件分割
如果我们需要使用lodash
内的一个方法, 那么我们就会引入该包, 然后调用, 看看这样引入后, 打包后的项目是什么样的.?
![build-1.4](E:\project\MyNote\_img\build\build-1.4.png)<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
{{ value }}
<HelloWorld msg="Welcome to Your Vue.js App" />
<HomeAdd ref="homeAdd"></HomeAdd>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
import _ from 'lodash' // 引入lodash
export default {
name: 'Home',
mounted() {
this.compute()
console.log('Home')
},
data() {
return {
value: ''
}
},
methods: {
compute() {
const arr = ['Turtle', 'Hello', 'World']
this.value = _.join(arr, '-') // 调用loadsh的join方法
}
},
components: {
HelloWorld,
HomeAdd: () => import(/* webpackChunkName: "home.add" */ './HomeAdd.vue')
}
}
</script>
发现chunk-vendors.[contenthash:8].js
从133kb
增加到了212kb
分割也可以应用于 javascript 插件的引用
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<button @click="compute">计算</button>
{{ value }}
<HelloWorld msg="Welcome to Your Vue.js App" />
<HomeAdd ref="homeAdd"></HomeAdd>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'Home',
mounted() {
console.log('Home')
},
data() {
return {
value: ''
}
},
methods: {
async compute() {
const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash')
const arr = ['Turtle', 'Hello', 'World']
this.value = _.join(arr, '-')
}
},
components: {
HelloWorld,
HomeAdd: () => import(/* webpackChunkName: "home.add" */ './HomeAdd.vue')
}
}
</script>
这里就会把lodash
单独打包出来, 减少了chunk-vendors.[contenthash:8].js
的体积
可以仔细查看DevTools的输出看到点击之后才请求该 js 资源
代码分割
在 Vue 项目中, 会把公用的 js 打包进一个chunk-vendors.[hash]:8.js
的文件中, 当引用的插件或者项目业务代码多的时候, 该文件会越来越大, 在浏览器加载的时候, 就会耗费更多时间, 导致页面首屏白屏时间过长
解决方案: 通过定向代码分隔, 把某些插件从该文件抽离, 单独成文件
技术点: 需要利用 Webpack 自带的 splitChunks
功能, 进行配置分割
配置项vue.config.js
module.exports = {
chainWebpack(config) {
config.when(process.env.NODE_ENV !== 'development', config => {
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
elementUI: {
name: 'chunk-elementUI',
priority: 20,
test: /[\\/]node_modules[\\/]_?element-ui(.*)/
}
}
})
})
}
}
一般的项目都会全量引入一个UI, 举例子为elementUI
,
以上配置会把elementUI
单独打包出来, 生成一个chunk-elementUI.[hash]:8.js
文件
大概会有600kb
的大小, 所以可以为主包减少这么多的体积
splitChunks
代码分割配置项详解
此配置对象代表SplitChunksPlugin
的默认配置
splitChunks: {
chunks: "async",
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
这里对于每一项进行解释
chunks
- 含义: 打包模式, 模式:
async
异步,all
全部 - 默认值:
async
异步, 只对异步引入的模块进行分割 all
: 不管是异步还是同步导入的模块, 都进行代码分割
minSize
- 含义: 当导入的模块需要超过该值, 才会进行代码分割
- 默认值: 30000字节
minChunks
- 含义: 一个模块需要被导入多少次才会进行代码分割
- 默认值: 1
- 注意: 该配置项只适用于同步引入的模块, 异步模块都会进行代码分割
maxAsyncRequests
- 含义: 同时加载的模块数是几个
- 举例: 当我们异步引入了10个类库, 按照正常情况下, 每个类库都会进行代码分割成一个单独的js文件(脱离
main.js
), 即生成了除main.js
外的10个分割js文件, 如果maxAsyncRequests: 5
则打包时, 前5个类库会进行代码分割, 生成对应的5个js文件, 后面的5个类库依然存在于main.js
中,不进行代码分割 - 默认值: 5
maxInitialRequests
- 含义: 对入口文件
entry
进行分割的时候, 最多能分割出多少个文件, 超出之后不进行分割 - 默认值: 3
maxAsyncRequests
与maxInitialRequests
区别
maxAsyncRequests
包含入口文件及其入口依赖文件(实际上也是模块)中所导入的模块的一起来统计是否超过maxAsyncRequests
设置的值maxInitialRequests
只是对入口文件中直接导入的模块进行统计
automaticNameDelimiter
- 含义: 文件名连接符号
- 默认值:
~
name
- 含义: 决定缓存组
cacheGroups
内的name
是否生效 - 默认值: true
cacheGroups
缓存组
Q: 什么是缓存组.?
A:
- 只对同步导入模块起作用, 把同步导入的模块根据相关配置进行分割缓存起来
- 没有设置缓存组, 那么会根据默认配置, 满足后才会分割
- 如果存在缓存组, 会把每个模块内符合缓存组配置项中模块先放到缓存组内, 等把全部模块都分析完毕, 再把符合缓存组配置项中模块全部打包在一起
- 对异步导入模块不起作用, 因为异步导入的模块都会单独生成一个模块
cacheGroups
继承 splitChunks
里的所有属性的值,如 chunks
、minSize
、minChunks
、maxAsyncRequests
、maxInitialRequests
、automaticNameDelimiter
、name
,我们还可以在 cacheGroups
中重新赋值,覆盖 splitChunks
的值
另外,还有一些属性只能在 cacheGroups
中使用:test
、priority
、reuseExistingChunk
text
- 含义: 正则匹配条件
priority
- 缓存组的优先级
- 数字越大, 等级越高
reuseExistingChunk
- 如果一个模块被打包了, 再遇上相同模块时会复用之前模块
开启gzip
压缩
gizp
压缩是一种http
请求优化方式,通过减少文件体积来提高加载速度
html
、js
、css
文件甚至json
数据都可以用它压缩,可以减小60%以上的体积
compression-webpack-plugin
webpack
在打包时可以借助 compression webpack plugin 实现gzip
压缩,首先需要安装该插件
yarn add -D compression-webpack-plugin
在vue.config.js
进行配置
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
configureWebpack: () => {
if (process.env.NODE_ENV === 'production') {
return {
plugins: [
new CompressionPlugin({
test: /\.js$|\.html$|\.css$|\.jpg$|\.jpeg$|\.png/, // 需要压缩的文件类型
threshold: 10240, // 对10K以上的进行压缩
deleteOriginalAssets: false // 是否删除原文件
})
]
}
}
}
}
安装后打包报错
TypeError: Cannot read property 'tapPromise' of undefined
原因是因为compression-webpack-plugin
这个版本高了,得降低点版本
yarn add -D compression-webpack-plugin@6.1.1
nginx
配置
在配置完Vue
部分后直接部署到nginx
上是不会生效的,还必须打开nginx
的gzip
功能才可以
首先你要准备配置一下nginx,在 http
中:
// nginx开启gzip服务
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
// 需要开启gzip的格式
gzip_types text/plain text/css application/json application/x-javascript text/xml
application/xml application/xml+rss text/javascript image/jpeg image/gif image/png image/jpg;
CDN引入
一般我们习惯于npm
引入包, 但是引入的包, 会在打包时候打包进去, 进而增加打包后的体积
那么在浏览器加载时候, 某些包过大, 就会导致页面白屏时间过长, 体验感较差
这时候就可以把某些包用 CDN
引入形式加载到项目内
配置如下:
const env = process.env.NODE_ENV === 'development' ? '' : '.min'
const cdn = {
css: [
'//unpkg.com/element-ui@2.13.2/lib/theme-chalk/index.css',
'//cdn.bootcdn.net/ajax/libs/animate.css/3.5.1/animate.css',
'/cdn/iconfont/1.0.0/index.css',
'/cdn/avue/2.7.3/index.css'
],
js: [
'/util/aes.js',
`//cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue${env}.js`,
`//cdn.jsdelivr.net/npm/vuex@3.6.0/dist/vuex${env}.js`,
`//cdn.jsdelivr.net/npm/vue-router@3.4.9/dist/vue-router${env}.js`,
`//unpkg.com/axios@0.21.0/dist/axios${env}.js`,
'//unpkg.com/element-ui@2.13.2/lib/index.js',
'/cdn/avue/2.7.3/avue.min.js',
'//cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js',
'//cdn.jsdelivr.net/npm/lodash@4.17.20/lodash.min.js',
'//unpkg.com/xlsx@0.16.9/dist/xlsx.min.js'
]
}
module.exports = {
pages: {
index: {
entry: 'src/main.js',
cdn: cdn
}
},
chainWebpack: config => {
//忽略的打包文件
config.externals({
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
axios: 'axios',
'element-ui': 'ELEMENT',
moment: 'moment',
lodash: '_',
xlsx: 'XLSX'
})
}
}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="format-detection" content="telephone=no">
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
<!-- 关键遍历 -->
<% for (let i in htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
<title>XXX</title>
</head>
<body>
<div id="app"></div>
<!-- 关键遍历 -->
<% for (let i in htmlWebpackPlugin.options.cdn.js) { %>
<script
type="text/javascript"
src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"
></script>
<% } %>
</body>
</html>
关键点:
-
config.externals
, 引入的包都会有全局注入的变量, 那么在页面引用该包时候, 指向引用该变量'vue-router': 'VueRouter'
意思引入
vue-router
这个包时候, 去找VueRouter
变量如何查看包注入的是哪个变量? 只能通过去查看源码, 导出的变量是哪个
-
pages.index.cdn: cdn
, 把CDN
引入放在一个变量内, 注入到``pages.index.cdn,
index.名称`该名称可以随意, 只是个遍历标识