1. 程式人生 > >Vue CLI 2&3 下的專案優化實踐 —— CDN + Gzip + Prerender

Vue CLI 2&3 下的專案優化實踐 —— CDN + Gzip + Prerender

前言

這些優化方案適用於 Vue CLI 2Vue CLI 3 ,文章主要基於Vue CLI 2進行介紹,關於如何在Vue CLI 3中進行相關的webpack調整,我已經放在了 vue-cli3-optimization 這個倉庫下,並配有詳細的註釋,且額外新增方便Sass使用的loader,使用Sass時無需再在每個需要引入變數和mixin的地方,每次都很麻煩的@import。下面將詳細介紹這些優化方案的實踐方式和效果:

和很多小夥伴一樣,我在開發Vue專案時也是基於官方[email protected]webpack模版,但隨著專案越做越大,依賴的第三方npm包越來越多,構建之後的檔案也會越來越大,尤其是vendor.js

,甚至會達到2M左右。再加上又是單頁應用,這就會導致在網速較慢或者伺服器頻寬有限的情況出現長時間的白屏。為了解決這個問題,我做了一些探索,在幾乎不需要改動業務程式碼的情況下,找到了三種有明顯效果的優化方案 —— CDN + Gzip + Prerender。我把這些方法整理了一下,放在了 Github倉庫 上,意圖通過不同的分支來展示不同的優化方式,對Vue專案效能的影響。你可以直接克隆下來試一試,也得益於有git歷史,你也可以很方便的檢視具體的改動細節。下面我將通過一個簡單的專案來展示這三種優化方案的效果。

一、首先準備一個簡單的專案

通過[email protected]webpack

模版生成,只包含最基礎的Vue三件套 ———— vuevue-routervuex以及常用的element-uiaxios。拆分兩個路由——“首頁”和“通訊錄”,通過axios非同步獲取一個通訊錄名單,並利用element-ui的表格展示。直接build,不做任何優化處理,以作參照。

1.1 構建後文件說明:

  1. app.css: 壓縮合並後的樣式檔案。
  2. app.js:主要包含專案中的App.vuemain.jsrouterstore等業務程式碼。
  3. vendor.js:主要包含專案依賴的諸如vuexaxios等第三方庫的原始碼,這也是為什麼這個檔案如此之大的原因,下一步將探索如何優化這一塊,畢竟隨著專案的開發,依賴的庫也能會越來越多。
  4. 數字.js:以0、1、2、3等數字開頭的js檔案,這些檔案是各個路由切分出的程式碼塊,因為我拆分了兩個路由,並做了路由懶載入,所以出現了0和1兩個js檔案。
  5. mainfest.jsmainfest的英文有清單、名單的意思,該檔案包含了載入和處理路由模組的邏輯

1.2 禁用瀏覽器快取,網速限定為Fast 3G下的Network圖(執行在本地的nginx伺服器上

可以看到未經優化的base版本在Fast 3G的網路下大概需要7秒多的時間才載入完畢

二、CDN 優化

為了更好的開發體驗,報錯捕獲,目前已經針對devbuild進行了區分,具體檢視git記錄,下面僅供參考。

  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的歷史記錄

2.1 比對新增 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. 數字.jsmainfest.js:這些檔案本來就很小,變化幾乎可以忽略。

2.2 同樣,禁用瀏覽器快取,網速限定為Fast 3G下的Network圖(執行在本地的nginx伺服器上

可以看出相同的網路環境下,載入從原來的7秒多,提速到現在的3秒多,提升非常明顯。而且更重要的一點是原本的方式,所有
jscss等靜態資源都是請求的我們自己的nginx伺服器,而現在大部分的靜態資源都請求的是第三方的CDN資源,
這不僅可以帶來速度上的提升,在高併發的時候,這無疑大大降低的自己伺服器的頻寬壓力,想象一下原來首屏900多KB的檔案
現在僅剩20KB是請求自己伺服器的!

使用Gzip兩個明顯的好處,一是可以減少儲存空間,二是通過網路傳輸檔案時,可以減少傳輸的時間。

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 [email protected],並把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的響應頭:

大家都是知道:常見的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 分支下,你可以克隆下來並在此基礎上開發你的專案。