Vue CLI 2&3 下的项目优化实践:CDN + Gzip + Prerender

2018 年 10 月 15 日 前端之巅
作者|郑昊川
编辑|覃云

本文经作者授权转载,原文链接:

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 秒多的时间才加载完毕。

二、CDN 优化

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 压缩

开启 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 的响应头:

四、Prerender 预渲染

大家都是知道:常见的 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.jsbuild中的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.htmlapp.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), 你可以克隆下来并在此基础上开发你的项目。

 活动推荐

今年12 月 7-8 日在北京国际会议中心举办的 ArchSummit 全球架构师技术峰会邀请了超过百位的国内外专业讲师,并设置了前端技术专题,分享他们的最新黑科技和研发经验。

目前大会 8 折优惠购票火热进行中,扫描以下图片二维码或点击“阅读原文”了解更多详情!

如有疑问欢迎咨询票务经理灰灰:17326843116(微信同号)


登录查看更多
0

相关内容

Best of 2016:VUE 获评 App Store 2016 年十佳应用。 被全球超过 120 个国家和地区 App Store 首页推荐,获得中国区 App Store 7 月最佳新应用称号。
FPGA加速系统开发工具设计:综述与实践
专知会员服务
63+阅读 · 2020年6月24日
【干货书】现代数据平台架构,636页pdf
专知会员服务
250+阅读 · 2020年6月15日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
115+阅读 · 2020年5月10日
【复旦大学-SP2020】NLP语言模型隐私泄漏风险
专知会员服务
24+阅读 · 2020年4月20日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
83+阅读 · 2019年11月25日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
20+阅读 · 2019年11月7日
【书籍】深度学习框架:PyTorch入门与实践(附代码)
专知会员服务
160+阅读 · 2019年10月28日
硬核实践经验 - 企鹅辅导 RN 迁移及优化总结
IMWeb前端社区
5+阅读 · 2019年5月6日
百度开源项目OpenRASP快速上手指南
黑客技术与网络安全
5+阅读 · 2019年2月12日
React Native 分包哪家强?看这文就够了!
程序人生
12+阅读 · 2019年1月16日
WebAssembly在QQ邮箱中的一次实践
IMWeb前端社区
13+阅读 · 2018年12月19日
教程 | PyTorch经验指南:技巧与陷阱
机器之心
15+阅读 · 2018年7月30日
刚开始学编程?这几款小工具能让你事半功倍
VIP会员
相关VIP内容
相关资讯
硬核实践经验 - 企鹅辅导 RN 迁移及优化总结
IMWeb前端社区
5+阅读 · 2019年5月6日
百度开源项目OpenRASP快速上手指南
黑客技术与网络安全
5+阅读 · 2019年2月12日
React Native 分包哪家强?看这文就够了!
程序人生
12+阅读 · 2019年1月16日
WebAssembly在QQ邮箱中的一次实践
IMWeb前端社区
13+阅读 · 2018年12月19日
教程 | PyTorch经验指南:技巧与陷阱
机器之心
15+阅读 · 2018年7月30日
刚开始学编程?这几款小工具能让你事半功倍
Top
微信扫码咨询专知VIP会员