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

本文永久连接:github.com/HaoChuan942…javascript

前言

这些优化方案适用于 Vue CLI 2Vue CLI 3 ,文章主要基于Vue CLI 2进行介绍,关于如何在Vue CLI 3中进行相关的webpack调整,我已经放在了 vue-cli3-optimization 这个仓库下,并配有详细的注释,且额外添加方便Sass使用的loader,使用Sass时无需再在每一个须要引入变量和mixin的地方,每次都很麻烦的@import。下面将详细介绍这些优化方案的实践方式和效果:php

和不少小伙伴同样,我在开发Vue项目时也是基于官方vue-cli@2webpack模版,但随着项目越作越大,依赖的第三方npm包愈来愈多,构建以后的文件也会愈来愈大,尤为是vendor.js,甚至会达到2M左右。再加上又是单页应用,这就会致使在网速较慢或者服务器带宽有限的状况出现长时间的白屏。为了解决这个问题,我作了一些探索,在几乎不须要改动业务代码的状况下,找到了三种有明显效果的优化方案 —— CDN + Gzip + Prerender。我把这些方法整理了一下,放在了 Github仓库 上,意图经过不一样的分支来展现不一样的优化方式,对Vue项目性能的影响。你能够直接克隆下来试一试,也得益于有git历史,你也能够很方便的查看具体的改动细节。下面我将经过一个简单的项目来展现这三种优化方案的效果。css

1、首先准备一个简单的项目

经过vue-cli@2webpack模版生成,只包含最基础的Vue三件套 ———— vuevue-routervuex以及经常使用的element-uiaxios。拆分两个路由——“首页”和“通信录”,经过axios异步获取一个通信录名单,并利用element-ui的表格展现。直接build,不作任何优化处理,以做参照。html

1.1 构建后文件说明:

  1. app.css: 压缩合并后的样式文件。
  2. app.js:主要包含项目中的App.vuemain.jsrouterstore等业务代码。
  3. vendor.js:主要包含项目依赖的诸如vuexaxios等第三方库的源码,这也是为何这个文件如此之大的缘由,下一步将探索如何优化这一块,毕竟随着项目的开发,依赖的库也能会愈来愈多。
  4. 数字.js:以0、一、二、3等数字开头的js文件,这些文件是各个路由切分出的代码块,由于我拆分了两个路由,并作了路由懒加载,因此出现了0和1两个js文件。
  5. mainfest.jsmainfest的英文有清单、名单的意思,该文件包含了加载和处理路由模块的逻辑

1.2 禁用浏览器缓存,网速限定为Fast 3G下的Network图(运行在本地的nginx服务器上

能够看到未经优化的base版本在Fast 3G的网络下大概须要7秒多的时间才加载完毕前端

2、CDN 优化

为了更好的开发体验,报错捕获,目前已经针对devbuild进行了区分,具体查看git记录,下面仅供参考。vue

  1. 将依赖的vuevue-routervuexelement-uiaxios这五个库,所有改成经过CDN连接获取。借助HtmlWebpackPlugin,能够方便的使用循环语法在index.html里插入jscssCDN连接。这里的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>
<% } %>
复制代码
  1. build/webpack.base.conf.js中添加以下代码,这使得在使用CDN引入外部文件的状况下,依然能够在项目中使用import的语法来引入这些第三方库,也就意味着你不须要改动项目的代码,这里的键名是importnpm包名,键值是该库暴露的全局变量。 webpack文档参考连接
externals: {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'element-ui':'ELEMENT',
    'axios':'axios'
  }
复制代码
  1. 卸载依赖的npm包,npm uninstall axios element-ui vue vue-router vuex
  2. 删除main.jselement-ui相关代码。

具体细节能够查看git的历史记录java

2.1 比对添加 CDN 先后构建的文件:

优化后: node

优化前:
能够看出:

  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. 数字.jsmainfest.js:这些文件原本就很小,变化几乎能够忽略。

2.2 一样,禁用浏览器缓存,网速限定为Fast 3G下的Network图(运行在本地的nginx服务器上

能够看出相同的网络环境下,加载从原来的7秒多,提速到如今的3秒多,提高很是明显。并且更重要的一点是本来的方式,全部 的jscss等静态资源都是请求的咱们本身的nginx服务器,而如今大部分的静态资源都请求的是第三方的CDN资源, 这不只能够带来速度上的提高,在高并发的时候,这无疑大大下降的本身服务器的带宽压力,想象一下原来首屏900多KB的文件 如今仅剩20KB是请求本身服务器的! webpack

3、Gzip 优化

使用Gzip两个明显的好处,一是能够减小存储空间,二是经过网络传输文件时,能够减小传输的时间。ios

3.1 如何开启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]\.";
}
复制代码

3.2 前端能为gzip作点什么

咱们都知道config/index.js里有一个productionGzip的选项,那么它是作什么用的?咱们尝试执行npm install --save-dev compression-webpack-plugin@1.x,并把productionGzip设置为true,从新build,放在nginx服务器下,看看有什么区别:

咱们会发现构建以后的文件多了一些js.gzcss.gz的文件,并且vendor.js变得更小了,这实际上是由于咱们开启了nginxgzip_static on;选项, 若是gzip_static设置为on,那么就会使用同名的.gz文件,不会占用服务器的CPU资源去压缩。

3.3 前端快速搭建基于nodegzip服务

没法搭建nginx环境的前端小伙伴也能够按以下步骤快速启动一个带gzipexpress服务器

  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')
  })
复制代码
  1. 执行node server.js

下图是express开启gzip的响应头:

4、Prerender 预渲染

你们都是知道:常见的Vue单页应用构建以后的index.html只是一个包含根节点的空白页面,当全部须要的js加载完毕以后,才会开始解析并建立vnode,而后再渲染出真实的DOM。当这些js文件过大而网速又很慢或者出现意料以外的报错时,就会出现所谓的白屏,相信作Vue开发的小伙伴们必定都遇到过这种状况。并且单页应用还有一个很大的弊端就是对SEO很不友好。那么如何解决这些问题呢?—— SSR固然是很好的解决的方案,但这也意为着必定的学习成本和运维成本,而若是你已经有了一个现成的vue单页应用,转向SSR也并非一个无缝的过程。那么预渲染就显得更加合适了。只须要安装一个webpack的插件 + 一些简单的webpack配置就能够解决上述的两个问题。

4.1 如何将单页应用转为预渲染

  1. 你须要将router设为history模式,并相应的调整服务器配置,这并不复杂
  2. npm i prerender-spa-plugin --save-dev

注意!!!预渲染须要下载 Chromium ,而因为你懂的缘由,谷歌的东西在国内没法下载,因此在根目录添加了.npmrc文件,来使用淘宝镜像下载。参考连接。若是你的终端能够翻到国外,直接忽略这一步,你也许会喜欢小飞机

  1. 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
    }
  })
复制代码
  1. config/index.jsbuild中的assetsPublicPath字段设置为'/',这是由于当你使用预渲染时,路由组件会编译成相应文件夹下的index.html,它会依赖static目录下的文件,而若是使用相对路径则会致使依赖的路径错误,这也要求预渲染的项目最好是放在网站的根目录下(这个坑我已经在prerender-spa-plugin仓库提过ISSUE了,不过借助postProcess,本身再写一个正则表达式,也能实现,若是你有这方面的需求,能够参考下面 路由懒加载带来的坑)。
  2. 调整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早已渲染好了静态页面。

4.2 效果如何?

和以前同样,咱们依然禁用缓存,将网速限定为Fast 3G(运行在本地的nginx服务器上)。能够看到,在vendor.js尚未加载完毕的时候(大概有700多kB,此时只加载了200多kB),页面已经完整的呈现出来了。事实上,只须要index.htmlapp.css加载完毕,页面的静态内容就能够很好的呈现了。预渲染对于这些有大量静态内容的页面,无疑是很好的选择。

4.3 路由懒加载带来的坑

若是你的项目没有作路由懒加载,那么你大可放心的按上面所说的去实践了。但若是你的项目里用了,你应该会看到webpackJsonp is not defined的报错。这个由于prerender-spa-plugin渲染静态页面时,也会将相似于<script src="/static/js/0.9231fc498af773fb2628.js" type="text/javascript" async charset="utf-8"></script>这样的异步script标签注入到生成的htmlhead标签内。这会致使它先于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(/<script[^<]*src="[^<]*[0-9]+\.[0-9a-z]{20}\.js"><\/script>/g,function (target) {
      console.log(chalk.bgRed('\n\n剔除的懒加载标签:'), chalk.magenta(target))
      return ''
    })
    return renderedRoute
  }
复制代码

除了这种解决方案,还有两种不推荐的解决方案:

  1. 索性不使用路由懒加载。
  2. HtmlWebpackPlugininject字段设置为'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-optimizationbase分支提交PR,好的方案我会采纳并整理。目前三种方案整合的最终结果我已经放在 master 分支下,你能够克隆下来并在此基础上开发你的项目。

相关文章
相关标签/搜索