對於一個網站來說開啟速度是一個很重要的指標,只是大部分時間內我們的精力可能都用來對付需求了,特別是當我們做的是一些內部的專案時,我們常常的會忽略了這一方面的優化。其實要對一個頁面的開啟速度做出一些比較常見的優化並沒有想象中的困難,本文將帶你做一些既不費力也不費時間的優化操作,這些操作中涉及到壓縮,快取,preload載入關鍵資源,prefetch快取懶載入資源與一些引用元件的建議及常見的工具庫處理。
開啟gzip壓縮
當我們使用webpack打包並壓縮js程式碼後,往往某些js(比如vendor)依然會很大,可能會達到1mb左右的大小,雖然以現在的頻寬來說如果我們是pc端專案這也不是什麼大問題,但是這裡面明顯是存在著相當大的優化空間的,gzip就是一種形式。
在瀏覽器的請求頭裡包含著這樣一句話Accept-Encoding:gzip, deflate
,這告訴我們瀏覽器是可以識別gzip壓縮的,使用gzip壓縮後的檔案將大大減小,很多情況下甚至能壓縮70%。現在的服務基本上都是使用nginx做轉發的,對於我們來說開啟gzip其實相當容易,只要配置如下的程式碼就可以了。
server {
gzip on;
gzip_types text/xml text/css text/plain text/javascript application/javascript application/x-javascript;
}
複製程式碼
上圖能看到沒開啟gzip時vendor近800k,而開啟gzip後大概只有250k。
可以說如果我們連gzip都沒有開啟的話,其他任何優化都顯得有點多餘,因為大概沒有另外一種優化方式能壓縮如此高的比例。
瀏覽器快取
除了常見的gzip壓縮外,另一個可以利用的優化點就是瀏覽器的快取。我會順帶著介紹一下瀏覽器的快取方式,但是不會過於詳細,有興趣的同學可以額外去找一些資料學習。
瀏覽器分為兩種快取,強快取與協商快取(也被稱為弱快取),其中協商快取不用我們自己配置,下面我們通過連續兩次重新整理頁面來觀察一下協商快取。
如上圖,在第一次的請求中nginx的http響應頭中攜帶了一個Last-Modified: sometime
那麼第二次的請求中瀏覽器的請求頭裡就會攜帶這個時間去對比,當nginx的時間在這個時間之前那麼就說明當前資源並沒有產生變化,返回的狀態碼也會對應的變成304,
當瀏覽器收到304後說明快取的資源並沒有過期,瀏覽器就會去讀取已經快取好的檔案。需要注意的是雖然瀏覽器最終是呼叫的快取,但是仍然存在http請求來確認該快取是否失效,所以很明顯還有另外的一種方式讓瀏覽器可以直接呼叫快取,不需要通過http請求,這就是強快取。強快取在nginx中可以通過如下程式碼配置
location ~* \.(css|js)$ {
proxy_set_header Host $host;
proxy_pass http://tomcat_xxx;
expires 7d;
}
location ~* \.(jpg|jpeg|png|gif|webp)$ {
proxy_set_header Host $host;
proxy_pass http://tomcat_xxx;
expires 30d;
}
複製程式碼
注意我們這裡使用的是tomcat,你可能需要的配置與我這個並不一樣,但是這並不關鍵,我們主要需要的是expires這項配置,他表示了我們希望快取的時間,我們配置的js與css快取時間為10天,而圖片則快取30天。一起開啟瀏覽器看一下效果。
這裡瀏覽器響應頭中會附帶一個max-age=604800,這裡的單位是秒,換算成天就是我們剛才設定的7天,再次重新整理瀏覽器後狀態碼依然是200但是後面多了一個from memory cache
表明此時是從記憶體中直接取出快取,並沒有傳送http請求,這對一些圖片與我們的依賴包vendor相當有用,我們完全可以給這兩個資源設定一個較大的快取時間,這樣當使用者訪問第一次後,這些資源始終會保持在使用者的快取中,就算我們之後更改了很多我們的業務程式碼,只要依賴沒有更改,使用者只用載入一些小的業務程式碼檔案就可以了,對於較大的vendor則依然可以從快取中獲取。
我們可以簡要的總結一下瀏覽器的快取方式並增加一些注意的點。瀏覽器會首先檢測強快取,如果命中則直接返回快取檔案,不會傳送http請求,如果沒有命中則去檢查弱快取,當弱快取命中時返回304狀態碼,瀏覽器依然從快取中獲取資源,如果弱快取也沒有命中則返回200狀態碼重新載入伺服器上的資源。
注意點:
- 強快取、弱快取只是名字上的區別並沒有什麼強弱之分,其實對於一般的瀏覽器來說重新整理就會使你當前請求資源的強快取失效,因為重新整理的時候會請求頭中會攜帶一個
max-age=0或是no-cache
,注意我這裡說的當前請求資源指的一般是你頁面的html文件,但是對於文件中外鏈的js與img等,不會因為重新整理導致強快取失效。不過如果你直接請求的是一個js檔案,那麼重新整理後這個js檔案強快取也會失效。 - 既然強快取不會發起http請求,那麼伺服器資源有變更的情況怎麼辦。其實webpack生成的hash碼就是幫我們解決這個問題用的,當外鏈的app.123456.js變成了app.654321.js瀏覽器自然會重新發起請求,這也提示了我們儘量不要去改變vendor導致vendor的hash變更產生快取失效的問題。
對於關鍵資源的優先載入與一些懶載入資源的預載入
由於我們的技術棧是vue,所以以下示例我們用vue來進行演示,但是本質上無論是什麼技術棧都是一樣的。 假設我們的專案是單頁面應用那麼首先應該優化的點就是路由的懶載入,也就是說不要一次性的將所有程式碼一起返回,只有切換到當前路由時我們才去請求當前路由對應的程式碼。對於vue-cli初始化的專案來說配置十分的簡單,在router中更改一下import的方式就可以。
const router = new Router({
routes: [
{
path: '/',
redirect: '/a',
},
{
path: '/a',
component: () => import('../components/a/index.vue'),
},
{
path: '/b',
component: () => import('../components/b/index.vue'),
},
]
複製程式碼
現在我們就可以根據我們訪問的router動態的載入js檔案了。但是這樣其實還有優化的空間,假設我們現在請求路由a,載入了vendor等公共js與a本身的js,那麼在訪問a頁面的空餘時間裡為什麼我們不將b路由的js也對應的載入到瀏覽器的快取中那,這樣當使用者切換到b路由時就可以不用在傳送http請求而是直接使用快取中的檔案就可以了。
在這裡我們要用到一個webpack外掛,PreloadWebpackPlugin
,這個外掛的作用是幫助我們對應的生成<link rel="preload" href="xxxx">
與<link rel="prefetch" href="xxxx">
標籤,其中preload中href的資源瀏覽器會優先的進行載入,關於preload的作用mdn文件是如此說的。
在瀏覽器的主渲染機制介入前就進行預載入。這一機制使得資源可以更早的得到載入並可用,且更不易阻塞頁面的初步渲染,進而提升效能。
具體相關其實就是瀏覽器的關鍵路徑的知識,這裡不詳述,可以另找資料。
而對於prefetch的href瀏覽器會進行預載入,同樣這裡引用mdn文件中的話對其描述
其利用瀏覽器空閒時間來下載或預取使用者在不久的將來可能訪問的文件。網頁向瀏覽器提供一組預取提示,並在瀏覽器完成當前頁面的載入後開始靜默地拉取指定的文件並將其儲存在快取中。當使用者訪問其中一個預取文件時,便可以快速的從瀏覽器快取中得到。
所以對於vue-cli生成的專案要做的是用preload載入vendor、manifest與app三個js而用prefetch去載入所有路由對應的檔案。這樣當我們訪問路由a時會首先下載需要的js與css,然後瀏覽器會自動的載入其他的路由檔案。此時當使用者去訪問其他路由時就不會點選時才去傳送請求。在webpack.prod.conf.js中加入如下程式碼,注意放在new HtmlWebpackPlugin()
的下面,由於我們的專案中只是用js與css組成的,你可以自己配置img與font這類資源。
new PreloadWebpackPlugin({
rel: 'prefetch',
}),
new PreloadWebpackPlugin({
rel: 'preload',
as(entry) {
if (/\.css$/.test(entry)) return 'style'
return 'script';
},
include: ['app', 'vendor', 'manifest']
})
複製程式碼
關於全域性元件的註冊
很多第三方元件中讓我們使用的方式是在main.js中引入元件,然後通過Vue.component()
來註冊全域性元件,其實很多情況下我們不應該採用這種方案,因為這會增大vendor的體積,特別是當此元件只是在其中的一個路由中用到,放入vendor中就更是不合理的,因為快取這個元件的程式碼對我們並沒有很多好處,假設頁面有5個路由,相當於進其他4個路由時這些程式碼都是沒有意義的,所以很多情況下區域性註冊元件將其打包進到路由程式碼中是更好的選擇。額外提一句,如果是echarts這種巨型庫,還是建議打包進vendor中的,因為由於業務程式碼總是在變更的,所以路由程式碼的hash值總是在變化,echarts這種重量的程式碼就不要每次上線都讓使用者重新載入一遍了...
關於這一點我再思考之後覺得我可能是錯的,因為像引入的第三方元件我們是作為外部依賴來使用的,就算是體積較小,每次打包進路由對應的js裡也會因為業務程式碼的變動導致使用者重新載入這部分程式碼,而體積大的時候就像我上文說的更不能打包進業務程式碼中。我能想到的場景只有一個情況可能有些不一樣,就是當程式碼上線後我們開發新的需求,需要引入新的依賴,但我們不希望使用者快取的vendor失效,所以我們可以打包一個新的依賴包出來,但這樣就提高了http的請求數量,我們知道在http1中我們優化的方向是域名分散與降低請求數,這樣到底好不好見仁見智吧,反正我應該是不會這麼做的。
一些工具庫可能需要我們做一些額外的處理
我們常見的工具庫比如lodash與moment其實都相當相當的大,而往往我們只是用幾個小功能而已,為了用這些小功能引入這麼巨大的庫真的有些浪費,這裡提一些基礎的解決方案。
lodash:安裝lodash-webpack-plugin
babel-plugin-lodash
在.babelrc中配置"plugins": ["lodash"]
,去按需引入lodash,但是說實話,按需引入有時候也不小,如果真的用到幾個很簡單的功能自己寫未必不是一個更好的選擇。
moment: 用day.js去替換。僅2kb大小的庫,與moment一樣的api,簡直不能再贊,我沒看原始碼,不知道2kb是怎麼做到與moment一樣的功能的,難道是moment實現的太笨了嗎...有點費解
還有類似echarts這種巨型的庫也有按需引入的方式,大家可以自己試著優化自己vendor的大小