这些优化方案适用于
Vue CLI 2
和Vue CLI 3
,文章主要基于Vue CLI 2
进行介绍,关于如何在Vue CLI 3
中进行相关的webpack
调整,我已经放在了 vue-cli3-optimization 这个仓库下,并配有详细的注释,且额外添加方便Sass
使用的loader
,使用Sass
时无需再在每个需要引入变量和mixin
的地方,每次都很麻烦的@import
。下面将详细介绍这些优化方案的实践方式和效果:
和很多小伙伴一样,我在开发Vue
项目时也是基于官方vue-cli@2
的webpack
模版,但随着项目越做越大,依赖的第三方npm
包越来越多,构建之后的文件也会越来越大,尤其是vendor.js
,甚至会达到2M
左右。再加上又是单页应用,这就会导致在网速较慢或者服务器带宽有限的情况出现长时间的白屏。为了解决这个问题,我做了一些探索,在几乎不需要改动业务代码的情况下,找到了三种有明显效果的优化方案 —— CDN
+ Gzip
+ Prerender
。我把这些方法整理了一下,放在了 Github仓库 上,意图通过不同的分支来展示不同的优化方式,对Vue
项目性能的影响。你可以直接克隆下来试一试,也得益于有git
历史,你也可以很方便的查看具体的改动细节。下面我将通过一个简单的项目来展示这三种优化方案的效果。
通过vue-cli@2
的webpack
模版生成,只包含最基础的Vue
三件套 ———— vue
、vue-router
、vuex
以及常用的element-ui
和axios
。拆分两个路由——“首页”和“通讯录”,通过axios
异步获取一个通讯录名单,并利用element-ui
的表格展示。直接build
,不做任何优化处理,以作参照。
app.css
: 压缩合并后的样式文件。app.js
:主要包含项目中的App.vue
、main.js
、router
、store
等业务代码。vendor.js
:主要包含项目依赖的诸如vuex
,axios
等第三方库的源码,这也是为什么这个文件如此之大的原因,下一步将探索如何优化这一块,毕竟随着项目的开发,依赖的库也能会越来越多。数字.js
:以0、1、2、3等数字开头的js
文件,这些文件是各个路由切分出的代码块,因为我拆分了两个路由,并做了路由懒加载,所以出现了0和1两个js
文件。mainfest.js
:mainfest
的英文有清单、名单的意思,该文件包含了加载和处理路由模块的逻辑Fast 3G
下的Network
图(运行在本地的nginx
服务器上可以看到未经优化的base
版本在Fast 3G
的网络下大概需要7秒多的时间才加载完毕
为了更好的开发体验,报错捕获,目前已经针对
dev
和build
进行了区分,具体查看git
记录,下面仅供参考。
vue
、vue-router
、vuex
、element-ui
和axios
这五个库,全部改为通过CDN
链接获取。借助HtmlWebpackPlugin
,可以方便的使用循环语法在index.html
里插入js
和css
的CDN
链接。这里的CDN
大部分使用的 jsDelivr 提供的。
<% for (var i in htmlWebpackPlugin.options.css) { %>
<% } %>
<% for (var i in htmlWebpackPlugin.options.js) { %>
<% } %>
build/webpack.base.conf.js
中添加如下代码,这使得在使用CDN
引入外部文件的情况下,依然可以在项目中使用import
的语法来引入这些第三方库,也就意味着你不需要改动项目的代码,这里的键名是import
的npm
包名,键值是该库暴露的全局变量。 webpack文档参考链接。 externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'element-ui':'ELEMENT',
'axios':'axios'
}
npm
包,npm uninstall axios element-ui vue vue-router vuex
main.js
里element-ui
相关代码。具体细节可以查看git
的历史记录
优化后:
优化前:
可以看出:
app.css
: 因为不再通过import 'element-ui/lib/theme-chalk/index.css'
,而是直接通过CDN
链接的方式引入element-ui
样式,使得文件小到了bytes
级别,因为它现在仅包含少量的项目的css
。app.js
:几乎无变化,因为这里面主要还是自己业务的代码。vendor.js
:将5个依赖的js
全部转为CDN
链接后,已经小到了不足1KB
,其实里面已经没有任何第三方库了。数字.js
和mainfest.js
:这些文件本来就很小,变化几乎可以忽略。Fast 3G
下的Network
图(运行在本地的nginx
服务器上可以看出相同的网络环境下,加载从原来的7秒多,提速到现在的3秒多,提升非常明显。而且更重要的一点是原本的方式,所有
的js
和css
等静态资源都是请求的我们自己的nginx
服务器,而现在大部分的静态资源都请求的是第三方的CDN
资源,
这不仅可以带来速度上的提升,在高并发的时候,这无疑大大降低的自己服务器的带宽压力,想象一下原来首屏900多KB的文件
现在仅剩20KB是请求自己服务器的!
使用Gzip
两个明显的好处,一是可以减少存储空间,二是通过网络传输文件时,可以减少传输的时间。
gzip
压缩开启gzip
的方式主要是通过修改服务器配置,以nginx
服务器为例,下图是,使用同一套代码,在仅改变服务器的gzip
开关状态的情况下的Network
对比图
未开启gzip
压缩:
开启gzip
压缩:
开启gzip
压缩后的响应头
从上图可以明显看出开启gzip
前后,文件大小有三四倍的差距,加载速度也从原来的7秒多,提升到3秒多
附上nginx
的配置方式
http {
gzip on;
gzip_static on;
gzip_min_length 1024;
gzip_buffers 4 16k;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;
gzip_vary off;
gzip_disable "MSIE [1-6]\.";
}
我们都知道config/index.js
里有一个productionGzip
的选项,那么它是做什么用的?我们尝试执行npm install --save-dev [email protected]
,并把productionGzip
设置为true
,重新build
,放在nginx
服务器下,看看有什么区别:
我们会发现构建之后的文件多了一些js.gz
和css.gz
的文件,而且vendor.js
变得更小了,这其实是因为我们开启了nginx
的gzip_static on;
选项,
如果gzip_static
设置为on
,那么就会使用同名的.gz
文件,不会占用服务器的CPU资源去压缩。
node
的gzip
服务无法搭建nginx
环境的前端小伙伴也可以按如下步骤快速启动一个带gzip
的express
服务器
npm i express compression
serve.js
,并粘贴如下代码 var express = require('express')
var app = express()
// 开启gzip压缩,如果你想关闭gzip,注释掉下面两行代码,重新执行`node server.js`
var compression = require('compression')
app.use(compression())
app.use(express.static('dist'))
app.listen(3000,function () {
console.log('server is runing on http://localhost:3000')
})
node server.js
下图是express
开启gzip
的响应头:
大家都是知道:常见的Vue
单页应用构建之后的index.html
只是一个包含根节点的空白页面,当所有需要的js
加载完毕之后,才会开始解析并创建vnode
,然后再渲染出真实的DOM
。当这些js
文件过大而网速又很慢或者出现意料之外的报错时,就会出现所谓的白屏,相信做Vue
开发的小伙伴们一定都遇到过这种情况。而且单页应用还有一个很大的弊端就是对SEO
很不友好。那么如何解决这些问题呢?—— SSR
当然是很好的解决的方案,但这也意为着一定的学习成本和运维成本,而如果你已经有了一个现成的vue
单页应用,转向SSR
也并不是一个无缝的过程。那么预渲染就显得更加合适了。只需要安装一个webpack
的插件 + 一些简单的webpack
配置就可以解决上述的两个问题。
router
设为history
模式,并相应的调整服务器配置,这并不复杂。npm i prerender-spa-plugin --save-dev
注意!!!预渲染需要下载
Chromium
,而由于你懂的原因,谷歌的东西在国内无法下载,所以在根目录添加了.npmrc
文件,来使用淘宝镜像下载。参考链接。如果你的终端可以翻到国外,直接忽略这一步,你也许会喜欢小飞机
build/webpack.prod.conf.js
下添加如下配置(没有路由懒加载的情况)。 const PrerenderSPAPlugin = require('prerender-spa-plugin')
...
new PrerenderSPAPlugin({
staticDir: config.build.assetsRoot,
routes: [ '/', '/Contacts' ], // 需要预渲染的路由(视你的项目而定)
minify: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
decodeEntities: true,
keepClosingSlash: true,
sortAttributes: true
}
})
config/index.js
里build
中的assetsPublicPath
字段设置为'/'
,这是因为当你使用预渲染时,路由组件会编译成相应文件夹下的index.html
,它会依赖static
目录下的文件,而如果使用相对路径则会导致依赖的路径错误,这也要求预渲染的项目最好是放在网站的根目录下(这个坑我已经在prerender-spa-plugin
仓库提过ISSUE
了,不过借助postProcess
,自己再写一个正则表达式,也能实现,如果你有这方面的需求,可以参考下面 路由懒加载带来的坑)。main.js
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app', true) // https://ssr.vuejs.org/zh/guide/hydration.html
执行npm run build
,你会发现,dist
目录和以往不太一样,不仅多了与指定路由同名的文件夹而且index.html
早已渲染好了静态页面。
和之前一样,我们依然禁用缓存,将网速限定为Fast 3G
(运行在本地的nginx
服务器上)。可以看到,在vendor.js
还没有加载完毕的时候(大概有700多kB,此时只加载了200多kB),页面已经完整的呈现出来了。事实上,只需要index.html
和app.css
加载完毕,页面的静态内容就可以很好的呈现了。预渲染对于这些有大量静态内容的页面,无疑是很好的选择。
如果你的项目没有做路由懒加载,那么你大可放心的按上面所说的去实践了。但如果你的项目里用了,你应该会看到webpackJsonp is not defined
的报错。这个因为prerender-spa-plugin
渲染静态页面时,也会将类似于这样的异步
script
标签注入到生成的html
的head
标签内。这会导致它先于app.js
,vendor.js
,manifest.js
(位于body
底部)执行。(async
只是不会阻塞后面的DOM
解析,这并不意味这它最后执行)。而且当这些js
加载完毕后,又会在head
标签重复创建这个异步的script
标签。虽然这个报错不会对程序造成影响,但是最好的方式,还是不要把这些异步组件直接渲染到最终的html
中。好在prerender-spa-plugin
提供了postProcess
选项,可以在真正生成html
文件之前做一次处理,这里我使用一个简单的正则表达式,将这些异步的script
标签剔除。本分支已经使用了路由懒加载,你可以直接查看git
历史,比对文件和base
分支的变化来对你的项目进行相应调整。
postProcess (renderedRoute) {
renderedRoute.html = renderedRoute.html = renderedRoute.html.replace(/