<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 服務器上)??梢钥吹剑趘endor.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小時內與您取得聯系。