在錯縱複雜的網絡環境下,如何將頁面快速得傳遞給用戶是前端們的職責,而在此以後,如何減小網絡傳輸的花費一樣值得咱們關注。本文以 HTTP/1.x 和 Service Worker 緩存兩個方面,就如何減小網絡傳輸成本爲目標,探討下筆者最近對於緩存的實踐,權當拋磚引玉 🤪css
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.html
根據 MDN 定義可知道,緩存是對已獲取資源的從新利用,是提高 WEB 性能的重要指標。根據是否和 Server 進行交互,HTTP 緩存分爲兩類:前端
強制緩存是無需和 Server 進行交互,直接在 Client 進行緩存。 而協商緩存須要和 Server 交互來判斷是否重用緩存。react
HTTP 緩存首部有如下幾種:webpack
Expires
Cache-Control
ETag/If-None-Match
Last-Modified/If-Modified-Since
語法:
Expires: <http-date>
git
Expires
經過設置一個時間戳,控制緩存的過時時間點。但缺點是客戶端時間和服務器時間可能不一致,沒法保證緩存的同步性。github
此外,若是存在 Cache-Control
首部並設置了max-age
指令,Expires
首部將被忽略。web
語法:`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,屬於強制緩存,再也不贅述。這裏只講下本身實踐所用到的設置項。json
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
是對資源的一個特殊標誌符,能惟一肯定資源。語法:
ETag: [W/]"<etag_value>"
複製代碼
W/
代表了資源是否採用弱類型驗證器進行比較,其較爲容易生成但不利於比較。"<etag_value>"
是對資源的惟一標誌符,其值是一串 ASCII 字符串。生成規則沒有必定的要求,但常採用的生成算法是內容的 hash 值加上內容的最後修改時間。
當響應頭部包含 ETag
時,下次請求時瀏覽器會自動帶上 If-None-Match: <last_etag_value>
首部,用來驗證資源是否過時。 若是已過時,則以 HTTP 200
返回新的內容響應並帶上新的 ETag
。若是資源未過時,則返回 HTTP 304
告知瀏覽器資源未過時能夠繼續使用。
語法:
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 對瀏覽器緩存進行檢查。
當下時間點,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 的基本部署,開啓其提供的緩存能力。
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
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)
})
複製代碼
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
複製代碼
其含義是不使用本地及任何中間存儲緩存,必須和服務器取得驗證才能拿到新的內容。
Cache-Control
對靜態資源進行長期緩存,配合 webpack 打包生成的文件 hash 名,可所有采用這一策略ETag/If-None-Match
對內容 hash 進行精確緩存Last-Modified/If-Modified-Since
對修改時間對內容進行緩存,以替代使用ETag/If-None-Match
對 CPU 的高消耗Service Worker
提供動態緩存和離線能力因此,如今開始打開調試工具,爲你的網站增長緩存吧~ ✌️