<script src="//cdn.bootcss.com/vue/2.3.2/vue.min.js"></script>
<script src="//cdn.bootcss.com/vue-router/2.5.3/vue-router.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.19.0/axios.min.js"></script>
<script src="https://unpkg.com/mint-ui/lib/index.js"></script>
<script src="https://unpkg.com/vue-i18n/dist/vue-i18n.js"></script>
externals: {
'vue':'Vue',
'axios':'axios',
'mint-ui': 'Mint',
'vue-router':'VueRouter',
'vue-i18n': 'VueI18n'
},
者|郑昊川
编辑|覃云
出处丨前端之巅
本文经作者授权转载,原文链接:
https://juejin.im/post/5b97b84ee51d450e6c7492f6?utm_medium=hao.caibaojian.com&utm_source=hao.caibaojian.com
这些优化方案适用于 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 仓库 上 (https://github.com/HaoChuan9421/vue-optimization), 意图通过不同的分支来展示不同的优化方式,对 Vue 项目性能的影响。你可以直接克隆下来试一试,也得益于有 git 历史,你也可以很方便的查看具体的改动细节。下面我将通过一个简单的项目来展示这三种优化方案的效果。
通过 vue-cli@2 的 webpack 模版生成,只包含最基础的 Vue 三件套 ———— vue、vue-router、vuex 以及常用的 element-ui 和 axios。拆分两个路由——“首页”和“通讯录”,通过 axios 异步获取一个通讯录名单,并利用 element-ui 的表格展示。直接 build,不做任何优化处理,以作参照。
构建后文件说明
1.app.css: 压缩合并后的样式文件。
2.app.js:主要包含项目中的 App.vue、main.js、router、store 等业务代码。
3.vendor.js:主要包含项目依赖的诸如 vuex,axios 等第三方库的源码,这也是为什么这个文件如此之大的原因,下一步将探索如何优化这一块,毕竟随着项目的开发,依赖的库也能会越来越多。
4 . 数字.js:以 0、1、2、3 等数字开头的 js 文件,这些文件是各个路由切分出的代码块,因为我拆分了两个路由,并做了路由懒加载 (https://router.vuejs.org/zh/guide/advanced/lazy-loading.html), 所以出现了 0 和 1 两个 js 文件。
5.mainfest.js:mainfest 的英文有清单、名单的意思,该文件包含了加载和处理路由模块的逻辑。
禁用浏览器缓存,网速限定为 Fast 3G 下的 Network 图 (运行在本地的 nginx 服务器上)
可以看到未经优化的 base 版本在 Fast 3G 的网络下大概需要 7 秒多的时间才加载完毕。
1. 将依赖的 vue、vue-router、vuex、element-ui 和 axios 这五个库,全部改为通过 CDN 链接获取。借助 HtmlWebpackPlugin, 可以方便的使用循环语法在 index.html 里插入 js 和 css 的 CDN 链接。这里的 CDN 大部分使用的 jsDelivr 提供的。
<!-- CDN 文件,配置在 config/index.js 下 --> <% for (var i in htmlWebpackPlugin.options.css) { %> <link href="<%= htmlWebpackPlugin.options.css[i] %>" rel="stylesheet"> <% } %> <% for (var i in htmlWebpackPlugin.options.js) { %> <script src="<%= htmlWebpackPlugin.options.js[i] %>"></script> <% } %>
2. 在 build/webpack.base.conf.js 中添加如下代码,这使得在使用 CDN 引入外部文件的情况下,依然可以在项目中使用 import 的语法来引入这些第三方库,也就意味着你不需要改动项目的代码,这里的键名是 import 的 npm 包名,键值是该库暴露的全局变量。
webpack 文档参考链接:https://webpack.js.org/configuration/externals/#src/components/Sidebar/Sidebar.jsx
externals: { 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'element-ui':'ELEMENT', 'axios':'axios' }
3. 卸载依赖的 npm 包,npm uninstall axios element-ui vue vue-router vuex;
4. 删除 main.js 里 element-ui 相关代码。
具体细节可以查看 git 的历史记录。
比对添加 CDN 前后构建的文件
优化后:
优化前:
可以看出:
1.app.css: 因为不再通过 import 'element-ui/lib/theme-chalk/index.css', 而是直接通过 CDN 链接的方式引入 element-ui 样式,使得文件小到了 bytes 级别,因为它现在仅包含少量的项目的 css。
2.app.js:几乎无变化,因为这里面主要还是自己业务的代码。
3.vendor.js:将 5 个依赖的 js 全部转为 CDN 链接后,已经小到了不足 1KB,其实里面已经没有任何第三方库了。
4 . 数字.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]\."; }
前端能为 gzip 做点什么?
我们都知道config/index.js里有一个productionGzip的选项,那么它是做什么用的?我们尝试执行npm install --save-dev compression-webpack-plugin@1.x, 并把productionGzip设置为true,重新build,放在 nginx 服务器下,看看有什么区别:
我们会发现构建之后的文件多了一些 js.gz 和 css.gz 的文件,而且 vendor.js 变得更小了,这其实是因为我们开启了 nginx 的 gzip_static on; 选项,如果 gzip_static 设置为 on, 那么就会使用同名的.gz 文件,不会占用服务器的 CPU 资源去压缩。
前端快速搭建基于 node 的 gzip 服务
无法搭建 nginx 环境的前端小伙伴也可以按如下步骤快速启动一个带 gzip 的 express 服务器。
1. 执行 npm i express compression;
2. 在项目根目录下新建一个 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') })
3. 执行 node server.js。
下图是 express 开启 gzip 的响应头:
大家都是知道:常见的 Vue 单页应用构建之后的 index.html 只是一个包含根节点的空白页面,当所有需要的 js 加载完毕之后,才会开始解析并创建 vnode,然后再渲染出真实的 DOM。
当这些 js 文件过大而网速又很慢或者出现意料之外的报错时,就会出现所谓的白屏,相信做 Vue 开发的小伙伴们一定都遇到过这种情况。而且单页应用还有一个很大的弊端就是对 SEO 很不友好。
那么如何解决这些问题呢?
SSR 当然是很好的解决的方案,但这也意为着一定的学习成本和运维成本,而如果你已经有了一个现成的 vue 单页应用,转向 SSR 也并不是一个无缝的过程。那么预渲染就显得更加合适了。只需要安装一个 webpack 的插件 + 一些简单的 webpack 配置就可以解决上述的两个问题。
如何将单页应用转为预渲染
1. 你需要将 router 设为 history 模式,并相应的调整服务器配置,这并不复杂(https://router.vuejs.org/zh/guide/essentials/history-mode.html)。
2.npm i prerender-spa-plugin --save-dev。
注意!!!预渲染需要下载 Chromium ,而由于你懂的原因,谷歌的东西在国内无法下载,所以在根目录添加了.npmrc 文件,来使用淘宝镜像下载。参考链接:https://github.com/cnpm/cnpmjs.org/issues/1246。如果你的终端可以翻到国外,直接忽略这一步, 你也许会喜欢小飞机:https://juejin.im/post/5b6852b1f265da0fb0189174。
3 . 在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 } })
4. 将config/index.js里build中的assetsPublicPath字段设置为'/',这是因为当你使用预渲染时, 路由组件会编译成相应文件夹下的index.html,它会依赖 static 目录下的文件,而如果使用相对路径则会导致依赖的路径错误,这也要求预渲染的项目最好是放在网站的根目录下(这个坑我已经在prerender-spa-plugin仓库提过ISSUE了,不过借助postProcess,自己再写一个正则表达式,也能实现,如果你有这方面的需求,可以参考下面 路由懒加载带来的坑)。
5. 调整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 src="/static/js/0.9231fc498af773fb2628.js" type="text/javascript" async charset="utf-8">< /script>这样的异步 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.replace(/<script.*src=".*[0-9]+\.[0-9a-z]*\.js"><\/script>/,'') return renderedRoute }
除了这种解决方案,还有两种不推荐的解决方案:
1 . 索性不使用路由懒加载。
2. 将HtmlWebpackPlugin 的 inject字段设置为'head',这样app.js,vendor.js,manifest.js就会插入到 head 里,并在异步的 script 标签上面。但由于普通的 script 是同步的,在他们全部加载完毕之前,页面是无法渲染的,也就违背了 prerender 的初衷,而且你还需要对 main.js 作如下修改,以确保 Vue 在实例化的时候可以找到<div id="app">< /div>,并正确挂载。
const app = new Vue({ // ... }) document.addEventListener('DOMContentLoaded', function () { app.$mount('#app') })
虽然官方的脚手架已经提供很多开箱即用的优化,比如 css 压缩合并,js 压缩与模块化,小图片转 base64 等等,但我们能做的还很多。我没有提及代码级别的优化细节,也是希望给大家提供一些可实践的方案。
上述三种方案或多或少都会给你项目带来一些收益。优化也是一门玄学,可研究的东西很多。也希望其他小伙伴可以在评论区提供宝贵意见,或者直接向我的这个项目 vue-optimization 的 base 分支提交 PR,好的方案我会采纳并整理。
目前三种方案整合的最终结果我已经放在 master 分支下 (https://github.com/HaoChuan9421/vue-optimization/tree/master), 你可以克隆下来并在此基础上开发你的项目。
CDN大家比较熟悉,这里做个简单介绍。
CDN主要是让用户访问资源的时候,能从离用户距离很近的CDN节点进行获取,不必到真正
提供服务的机器上获取。所以CDN可以
CDN经历了三个阶段
第一阶段 1995年,互联网发明者Tim,创建了第一家CDN服务公司Akamai
第二阶段 1999~2001,互联网发展高潮期,CDN快速发展
第三阶段 2001年互联网破灭,CDN公司大量倒闭,Tim的公司也倒闭了。2002年开始,宽带提升、游戏和视频大发展,带动CDN大发展
CDN虽然经历了二十多年的发展,但是现在还没有形成标准规范,各家的具体实现也不一样,本文章只讲解一种类型,希望能够让大家更深入的了解CDN。
CDN的请求过程,大致如下图所示,下面我简单介绍。左侧的实线框是DNS查找阶段,右侧的虚线框是CDN的范围。https://www.processon.com/view/link/5ed5175e0791291d5dba30ea
用户在浏览器请求某个链接,如event.mi.com,浏览器需要查找该域名对应的ip地址
1.先查找自己机器的DNS客户端上是否有记录,如果没有记录
2.从本地DNS服务器获取,如果本地DNS服务器没有记录
3.从根域名服务器查找(根域名服务器全球共13台),根域名返回com域名服务器所在地址
4.本地DNS服务器从com域名服务器查找,com域名服务器返回event.mi.com的权威域名服务器地址
- 权威域名DNS服务器:包含了该域名的所有信息
5.找到权威域名服务器后,会查到该域名有个CNAME,这个CNAME一般指向CDN的全局负载均衡系统
- DNS的A记录
1)A记录格式为“域名-ip”,记录的是该域名对应的服务器ip地址
2)DNS的CNAMECNAME为域名的别名,一般有两种作用1)多个域名指向同一个服务ip,当服务ip变动时,只需要改一个A记录即可。例如,域名www.abc.com的A记录为1.1.1.1,对于域名mail.abc.com和study.abc.com的别名可以设置为www.abc.com,这样当该服务变更ip地址时,只需要变更www.abc.com的A记录,其他域名无需变动,减少维护成本2)CNAME在CDN上的作用也很重要。将域名挂载在CDN上,需要将该域名的CNAME设置为CDN供应商提供的域名,这样CDN供应商才能通过DNS将流量转移到CDN上。而且域名的CNAME设置为CDN的域名后,该域名的A记录也不能存在了。一般通过nslookup和dig命令,可以查看DNS解析的情况,如下图所示,可以看到域名event.mi.com的CNAME为白山云的一个域名。另外可以看到白山云的域名还有对应的CNAME,这个主要是为了做负载均衡,后面会进行讲解。
3) 本模块讲述的DNS解析是使用迭代查找,DNS还提供递归查找的方法, 大家如果有兴趣,可以看一下两者的差异
通过上面讲述的DNS解析过程,CDN运营商成功将请求转移到他们那里
1.CDN的全局负载均衡系统实现方式很多,这里讲述使用的比较普遍的方案,基于DNS的全局负载均衡系统。
2.客户端请求CDN区域负载均衡系统,该系统会确定提供服务的CDN缓存服务器。区域负载均衡系统一般使用动态策略,为此需要有单独的服务器来收集区域内CDN缓存服务器的各种信息(如会话能力、往返时间、流量、缓存所在位置等)
3.如果区域负载均衡系统提供的CDN缓存服务器没有缓存或者缓存失效,则会向上一级CDN缓存服务器进行请求,一般使用的协议有ICP/HTCP/CARP等。当然,判断缓存失效使用的是Web基础知识,Pragma、 Expires、Cache-Control、Last-Modified、Etag等
4.如果上层的CDN缓存服务器仍然没有缓存或者过期,则会到回源机上请求该文件,请求成功后进行缓存
CDN对抗高并发的情况是很有用的,可以参考这篇文章《常用缓存技巧》
但是使用CDN的时候,也可能遇到一些问题,在这里我给大家讲述一些我曾经遇到的问题
最近遇到一个问题,通过CDN获取50KB的图片,耗时120s。
发生这种情况的原因是CDN厂商负载均衡配置有问题,在错误的配置下,为了拿到该图片,需要经过半个地球。后来让CDN厂商修改配置后,只需要0.2s。
商品需要有产品站,产品站会引用js文件,每次产品站变更,js名称不变,但js的tag会变,如从base.js?v01变为base.js?v02。js文件设置的是永不过期,如果版本号变更,则会回源,这是前提。
如果产品站和js版本不匹配,产品站会产生一些错误,如打不开页面或者某些功能无法使用。产品站和js不匹配有两种情况
1.产品站为新版本,js为老版本
这种情况一般是因为发布的时候,没有先将js发布到回源机上,而是先发布了新的产品站,这样新的产品站请求新的js时,会请求到回源机,而回源机还是老版本,这样老的js被当做新的js缓存了。这种情况发生后,一般生成新的js版本,重新走一次发布操作。
2.产品站为老版本,js为新版本
这种情况产生的原因比较多,也往往比较难以处理。发生这种情况的一个前提是已经发布新的js到回源机上,老的产品站还没有发布
这种情况发生后,往往会将服务器压垮。发生这种情况的原因是因为大部分CDN服务商,判断命中CDN的方式是根据整个url,如果该url通过google广告等推广,后面会添加不同的后缀,会无法命中。阻止这种情况的发生,可以让运维帮忙做特殊配置,只有指定的query参数变化才回源(运维很可能不想做这种操作,因为不利于后期维护),或者提升自己的服务性能。
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
1. 记博客服务被压垮的历程
2. 常用缓存技巧
3. 如何高效对接第三方支付
4. Gin框架简洁版
5. 关于代码review的思考
*请认真填写需求信息,我们会在24小时内与您取得联系。