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 分支下,你能夠克隆下來並在此基礎上開發你的項目。

相關文章
相關標籤/搜索