在錯縱複雜的網路環境下,如何將頁面快速得傳遞給使用者是前端們的職責,而在此之後,如何減少網路傳輸的花費同樣值得我們關注。本文以 HTTP/1.x 和 Service Worker 快取兩個方面,就如何減少網路傳輸成本為目標,探討下筆者最近對於快取的實踐,權當拋磚引玉 ?
HTTP 快取
The performance of web sites and applications can be significantly improved by reusing previously fetched resources. Web caches reduce latency and network traffic and thus lessen the time needed to display a representation of a resource. By making use of HTTP caching, Web sites become more responsive.
根據 MDN 定義可知道,快取是對已獲取資源的重新利用,是提升 WEB 效能的重要指標。根據是否和 Server 進行互動,HTTP 快取分為兩類:
- 強制快取
- 協商快取
強制快取是無需和 Server 進行互動,直接在 Client 進行快取。 而協商快取需要和 Server 互動來判斷是否重用快取。
HTTP 快取首部有以下幾種:
Expires
Cache-Control
ETag/If-None-Match
Last-Modified/If-Modified-Since
Expires
語法:
Expires: <http-date>
Expires
通過設定一個時間戳,控制快取的過期時間點。但缺點是客戶端時間和伺服器時間可能不一致,無法保證快取的同步性。
此外,如果存在 Cache-Control
首部並設定了max-age
指令,Expires
首部將被忽略。
Cache-Control
語法:`Cache-Control: [public | private | no-cache | only-if-cached],max-age=|s-maxage=|max-stale[=]|min-fresh=][,must-revalidate|proxy-revalidate|immutable][,no-store|no-transform]
具體配置細節見 MDN,屬於強制快取,不再贅述。這裡只講下自己實踐所用到的設定項。
public | private
max-age=<seconds>
no-cache | no-store | must-revalidate
public
和 private
定義了快取的共享性,分為共享(public)與私有(private)快取。共享快取儲存的響應能夠被多個使用者使用,私有快取只能用於單獨使用者。 共享快取可存在於 ISP、閘道器或 CDN 的節點上,能很大程度快取熱門資源,減少網路擁堵與延遲,但存在中間人攻擊的風險,故存在private
快取 —— 只快取在使用者的瀏覽器端,不會被共享。可根據自己的業務需求,選擇是私有還是共享的。
max-age=<seconds>
規定了快取時長,以秒為單位。從開始接收到資源為時間點,在接下來的 max-age
時間內使用快取。理論上來說可以長期快取,但帶來的問題是瀏覽器快取的臃腫,根據 RFC2616 最長時常設為一年較為合適,即 Cache-Control: max-age=31536000
。
no-cache
、no-store
和 must-revalidate
。no-cache
規定使用快取之前時一定要經過驗證,比如驗證 ETag/ Last-Modified
等; no-store
直接禁止瀏覽器以及所有中間快取儲存任何版本的返回響應,每次使用者請求該資產時,都會向伺服器傳送請求,並下載完整的響應;must-revalidate
快取必須在使用之前驗證舊資源的狀態,並且不可使用過期資源。
ETag/If-None-Match
ETag
是對資源的一個特殊標誌符,能唯一確定資源。語法:
ETag: [W/]"<etag_value>"
複製程式碼
W/
表明了資源是否採用弱型別驗證器進行比較,其較為容易生成但不利於比較。"<etag_value>"
是對資源的唯一標誌符,其值是一串 ASCII 字串。生成規則沒有一定的要求,但常採用的生成演算法是內容的 hash 值加上內容的最後修改時間。
當響應頭部包含 ETag
時,下次請求時瀏覽器會自動帶上 If-None-Match: <last_etag_value>
首部,用來驗證資源是否過期。 如果已過期,則以 HTTP 200
返回新的內容響應並帶上新的 ETag
。如果資源未過期,則返回 HTTP 304
告知瀏覽器資源未過期可以繼續使用。
Last-Modified/If-Modified-Since
語法:
Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
顧名思義,Last-Modified/If-Modified-Since
是根據內容最後的修改時間來判斷是否採用快取的方法。但由於最小時間單位為秒,對於要求時間比較精細的資源可能不太適用。
快取優先順序
HTTP/1.x 快取首部的優先順序: Cache-Control
> Expires
> ETag/If-None-Match
> Last-Modified/If-Modified-Since
, 即在同時設定了上述首部時Cache-Control
最高,可根據業務需求設定。
以上,便是 HTTP/1.x 快取設定的首部解釋,可以通過Browser Caching Checker 對瀏覽器快取進行檢查。
Server Worker 快取
當下時間點,Service Worker 在瀏覽器上的支援度已高達 86.16%, 所以是時候考慮開啟 Service Worker 來加速你的網站了。不僅可以利用 Service Worker 所帶來的快取好處,還能很容易遷移到 PWA,更大程度發掘 Web App 的能力。
不同於 HTTP 快取,Server Worker 不僅能動態快取資源,而且還能提供 offline 模式,對弱網路環境的使用者極為友好。開啟 Service Worker 大概需要註冊、安裝、快取資源、更新和登出等過程。
接下來以一個小 Demo 為例,簡單介紹如何開啟一個 Service Worker 服務。原始碼見 sw-cache-example
註冊
註冊流程很簡單,只需要判斷瀏覽器是否支援 Service Worker 特性,並在頁面 Load 之後,註冊 Service Worker 服務,關鍵程式碼:
// sw-reg.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('./sw.js').then(
function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope)
},
function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err)
}
)
})
}
複製程式碼
安裝
安裝過程需要做的有:監聽 install
事件,並在其回撥事件內快取資源。
var CACHE_NAME = 'cache-v1'
var urlsToCache = ['/', '/styles/main.css', '/script/main.js']
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
console.log('Opened cache')
return cache.addAll(urlsToCache)
})
)
})
複製程式碼
響應快取
最重要的一步,就是在資源被快取後利用快取了。需要做的也很簡單:監聽 fetch
事件 -> 對已快取的資源進行響應。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
if (response) {
return response
}
return fetch(event.request)
})
)
})
複製程式碼
更新
更新也是 Service Worker 很重要的一步,其過程也很易懂:驗證資源是否過期 -> 對過期的資源進行刪除並快取新的資源。
self.addEventListener('activate', function(event) {
var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1']
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName)
}
})
)
})
)
})
複製程式碼
登出
登出只需要拿到 Service Worker 例項,呼叫 unregister
即可。
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister()
})
}
複製程式碼
至此,基本完成了 Service Worker 的基本部署,開啟其提供的快取能力。
? 實踐過程中遇到的坑
- 遷移 HTTP 請求方法為
fetch
由於在響應快取時,需要通過監聽 fetch
事件來響應快取,故需要更改 HTTP 請求方法為 fetch
,其 API 參見 MDN。 對於不支援 fetch
的瀏覽器,可以使用這個 fetch 進行打補丁。
- 取消
fetch
請求
由於 fetch
沒有提供原生的取消方法,故需要使用 signal 來取消 fetch
請求。
const controller = new AbortController()
const signal = controller.signal
fetch('/some/url', { signal })
.then(res => res.json())
.then(data => {
// do something with "data"
})
.catch(err => {
if (err.name == 'AbortError') {
return
}
})
// controller.abort(); // can be called at any time
複製程式碼
Polyfill 參照 abortcontroller-polyfill
- 增加 Service Worker 開關 Service Worker 提供的快取雖然好用,但有時候需要根據業務登出 Service Worker, 這時就需要一個開關來控制。而且應該在第一次部署的時候就增加開關,對於快取進行控制。
fetch(API.switch)
.then(res => {
const isOn = res.status
if (isOn) {
sw.register()
} else {
sw.unregister()
}
})
.catch(err => {
console.error('fetch sw status error', err)
})
複製程式碼
- 對入口檔案取消快取 對於一般的 SPA,是通過入口檔案進行資源的索引,所以對入口檔案應該不予快取,並要求其強制更新。在使用sw-precache-webpack-plugin應排除入口檔案:
new SWPrecacheWebpackPlugin(
{
cacheId: 'my-project-name',
dontCacheBustUrlsMatching: /\.\w{8}\./,
filename: 'service-worker.js',
minify: true,
navigateFallback: PUBLIC_PATH + 'index.html',
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/, /index\.html$/],
}
),
複製程式碼
對入口檔案可以設定 HTTP 響應首部:
Cache-Control: no-cache, no-store, must-revalidate
複製程式碼
其含義是不使用本地及任何中間儲存快取,必須和伺服器取得驗證才能拿到新的內容。
- 如果不想自己編寫 Service Worker, 可以參照網上的模板或外掛 ?:
總結
- 使用
Cache-Control
對靜態資源進行長期快取,配合 webpack 打包生成的檔案 hash 名,可全部採用這一策略 - 使用
ETag/If-None-Match
對內容 hash 進行精確快取 - 對於時間要求不精確的資源,使用
Last-Modified/If-Modified-Since
對修改時間對內容進行快取,以替代使用ETag/If-None-Match
對 CPU 的高消耗 - 使用
Service Worker
提供動態快取和離線能力
所以,現在開始開啟除錯工具,為你的網站增加快取吧~ ✌️