本文翻譯自:jakearchibald.com/2016/cachin…javascript
這是一篇2016年的老文章。做者是Chrome瀏覽器的開發成員。css
本文首發於公衆號:符合預期的CoyPanjava
使用正確的緩存能夠帶來巨大的頁面性能上的收益,節省帶寬,減小服務器成本。可是許多網站並無解決好他們的緩存問題,創造了一個race conditions,致使相互依賴的資源之間失去了同步。web
絕大多數緩存的最佳實踐,都屬於下面兩種模式:gulp
Cache-Control: max-age = 31536000
複製代碼
在這種模式下,你不會去改變特定url下的文件內容,你直接改變url:瀏覽器
<script src="/script-f93bca2c.js"></script>
<link rel="stylesheet" href="/styles-a837cb1e.css">
<img src="/cats-0e9a2ef4.jpg" alt="…"> 複製代碼
每個URL都包含一個跟隨文件內容變換的部分。這個部分能夠是版本號,修改日期,或者文件內容的hash值。緩存
大多數服務端框架都有工具能夠簡單的實現這個需求。Node.js下還有更輕量級的工具可以作到一樣的事情,好比gulp-rev.安全
可是,這種模式不適合諸如文章、博客這樣的場景。文章和博客的URL是不會有版本號的,並且他們的內容可以隨時修改。說真的,若是我在文章中犯了拼寫或者語法錯誤,那麼我須要可以快速、頻繁的修改文章內容。bash
Cache-Control: no-cache
複製代碼
注意:
no-cache
並不意味着不緩存,而是使用緩存前必須請求服務端進行檢查(或者說叫從新校驗)。no-store
告訴瀏覽器,根本不要緩存這個文件。同時,must-revalidate
也不是說就『must-revalidate』,而是若是本地資源的緩存時間尚未超過設置的max-age的值,就能夠直接使用本地資源,不然必須從新校驗。服務器
在這種模式下,你能夠在響應頭裏添加一個ETag(你選擇的版本ID)或者Last-Modified。客戶端下一次請求資源時,會分別帶上If-None-Match和If-Modified-Since,服務端會判斷說:直接使用你已有的本地資源吧,他們是最新的。這就是最多見的:HTTP 304
若是沒有帶上ETag/Last-Modified,服務端會再次返回完成的內容。
這種模式老是會發起一個網絡請求,而模式一是能夠不用經過網絡的。
使用模式一時,由於網絡基礎建設而致使的延時是很常見的,使用模式二時,也很容易遇到網絡環境帶來的延遲。取而代之的是中間的東西:一個短期的max-age設置和可變的內容。這是一種十分糟糕的妥協。
不幸的是,這種作法並不是不常見。好比,Github pages就是這樣的。
想象一下有如下三個url:
服務端都是返回的:
Cache-Control: must-revalidate, max-age=600
複製代碼
這種模式在測試的時候看起來是能夠的,但在現實中,會出問題,而且很難追蹤。在上面的例子中,服務端確實已經更新了HTML, CSS 和JS,可是頁面最終使用了緩存裏的HTML,JS,CSS倒是從服務端獲取的最新的版本。資源版本不匹配致使了頁面出錯。
一般狀況下,當咱們對HTML進行重大更改時,咱們還可能更改HTML對應的CSS結構,並更新JS以適應樣式和內容的更改。這些資源是相互依賴的,可是緩存的header是沒法描述這種依賴的。用戶最終看到的,多是一兩個新版本的資源,和其餘老的資源。
max-age和響應時間有關,所以,若是上述全部的資源都是在同一次訪問中請求的,他們大概會在同一時間到期,可是仍然有很小的可能發生競爭。若是你的某些頁面並不包含JS或者包含了不一樣的CSS,那麼過時時間可能就不一樣步了。更糟糕的是,更糟糕的是,瀏覽器老是從緩存中刪除東西,它不知道HTML、CSS和JS是相互依賴的,因此它會很高興地刪除一個而不是其餘的。上述的狀況,均可能會致使頁面資源的版本不匹配。
對用戶來講,他們最終會看到錯誤的頁面佈局和錯誤的頁面功能,從細微的錯誤到徹底不可用的內容。
謝天謝地,對用戶來講仍是有補救措施的。
若是頁面做爲刷新的一部分加載,瀏覽器會忽略max-age,向服務器進行驗證。所以,若是用戶遭遇了由於max-age而形成的錯誤,刷新是能夠解決問題的。固然,強迫用戶這樣作會下降信任度,由於這會讓你感受到你的網站是不靠譜的。
假設你有如下的service worker:
const version = '2';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => cache.addAll([
'/styles.css',
'/script.js'
]))
);
});
self.addEventListener('activate', event => {
// …delete old caches…
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
複製代碼
這個service-worker
若是咱們更改了CSS/JS,咱們會修改service-worker中的版本號,觸發service-worker的更新。可是,假如addAll發出的請求通過了HTTP緩存(和其餘大多數緩存同樣),咱們也會進入到max-age的race condition,緩存不匹配的CSS、JS版本。
一旦他們被緩存了,咱們將會一直看到不匹配的CSS和JS,直到咱們下一次更新service-worker。而在下一次更新時,咱們可能還會陷入另外一個race condition。
你能夠在service worker中跳過緩存:
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => cache.addAll([
new Request('/styles.css', { cache: 'no-cache' }),
new Request('/script.js', { cache: 'no-cache' })
]))
);
});
複製代碼
不幸的是,這個緩存的設置在Chrome/Opera中還不支持,Firefox也是剛剛支持。你能夠本身來實現相似的功能:
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => Promise.all(
[
'/styles.css',
'/script.js'
].map(url => {
// cache-bust using a random query string
return fetch(`${url}?${Math.random()}`).then(response => {
// fail on 404, 500 etc
if (!response.ok) throw Error('Not ok');
return cache.put(url, response);
})
})
))
);
});
複製代碼
在上述代碼中,我用隨機數來避免緩存,可是你能夠更進一步,在構建的時候爲內容增長一個hash值(和sw-precache作的事差很少)。這是一種在js層面的對模式一的實現,可是僅僅對service worker的使用者是有效的,而不是對全部的瀏覽器和你的CDN都有效。
正如你所見,你能夠繞過service worker中糟糕的緩存,可是你最好解決根源的問題。正確的設置緩存可以讓你在使用service worker的時候更加輕鬆,而且對那些不支持service worker的瀏覽器也是有好處的,還能讓你充分的使用你的CDN。
正確的緩存頭還意味着你能夠大量簡化server worker的更新:
const version = '23';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => cache.addAll([
'/',
'/script-f93bca2c.js',
'/styles-a837cb1e.css',
'/cats-0e9a2ef4.jpg'
]))
);
});
複製代碼
在這裏,我將使用模式2(服務器從新驗證)緩存根頁面,其他資源使用模式1(不可變內容)。每次service worker更新都將觸發對根頁面的請求,但只有當資源的URL發生更改時,纔會下載其他資源。這很好,由於不管你是從之前的版本仍是第10個版本更新,它均可以節省帶寬並提升性能。
相對於本地應用來講,這是一個巨大的優點。在本地應用中,無論二進制內容有細微和巨大的改變,整個二進制內容都會被下載。而在這裏,咱們只須要一個小小的下載,就能更新巨大的web app.
service worker的工做最好是做爲一個加強方案,而不是變通方案。因此預期與緩存抗爭,不如好好利用緩存。
對於可變內容使用max-age通常狀況下是一個錯誤的選擇,但也不老是這樣。好比,這個頁面設置了一個3分鐘的max-age. race condition在這個頁面是不會成爲問題的,由於這個頁面沒有任何遵循這一種模式的依賴(個人css,js,圖片等都遵循模式1-不可變內容),依賴於此頁的任何內容都不會遵循相同的模式。
這種模式意味着,若是我有幸寫了一篇熱門文章,個人cdn可讓個人服務器散熱,而我能忍受用戶須要花三分鐘時間纔看到文章更新。
這種模式不能隨便使用。若是我在文章中添加了一個新的部分,而且將這個部分連接到一篇新的文章,那麼我就創造了一個會爭用的依賴項。用戶能夠單擊連接,並在沒有引用部分的狀況下獲取文章的副本。若是我想避免這種狀況,我就得更新第一篇文章,刷新cdn, 等待3分鐘,而後在另外一篇文章中添加指向他的連接。是的…..你必須很是當心這種模式。
正確使用,緩存能極大的提升性能而且較少帶寬消耗。對於任何容易更改的URL,都支持不可變的內容,不然在服務器從新驗證時會使其安全。只有當你足夠勇敢,而且你確信你沒有可能會失去同步的依賴項時,再使用max-age和可變內容的模式。