【譯】緩存最佳實踐 & max-age的陷阱

本文翻譯自:jakearchibald.com/2016/cachin…javascript

這是一篇2016年的老文章。做者是Chrome瀏覽器的開發成員。css

本文首發於公衆號:符合預期的CoyPanjava

使用正確的緩存能夠帶來巨大的頁面性能上的收益,節省帶寬,減小服務器成本。可是許多網站並無解決好他們的緩存問題,創造了一個race conditions,致使相互依賴的資源之間失去了同步。web

絕大多數緩存的最佳實踐,都屬於下面兩種模式:gulp

模式一:不可變的內容 ,長時間的max-age

Cache-Control: max-age = 31536000
複製代碼
  • 同一個URL對應的內容永不改變
  • 瀏覽器/CDN 能夠緩存這個資源長達一年的時間
  • 被緩存資源的存儲時間小於max-age指定的秒數時,該資源能夠直接被使用而無需通過服務器。

在這種模式下,你不會去改變特定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
複製代碼
  • 同一個url對應的內容會改變
  • 任何本地緩存的版本都是不可信的,除非服務器校驗經過

注意: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設置和可變的內容。這是一種十分糟糕的妥協。

對可變內容使用max-age一般是一個錯誤的選擇

不幸的是,這種作法並不是不常見。好比,Github pages就是這樣的。

想象一下有如下三個url:

  • /article/
  • /styles.css
  • /scripts.js

服務端都是返回的:

Cache-Control: must-revalidate, max-age=600
複製代碼
  • url對應的內容是變了
  • 若是瀏覽器緩存了一個資源版本,可是沒有到10分鐘,會不通過服務器直接使用這個緩存的資源。
  • 不然發起一個網絡請求,帶上If-Modified-Since或者If-None-Match(若是可用)

這種模式在測試的時候看起來是能夠的,但在現實中,會出問題,而且很難追蹤。在上面的例子中,服務端確實已經更新了HTML, CSS 和JS,可是頁面最終使用了緩存裏的HTML,JS,CSS倒是從服務端獲取的最新的版本。資源版本不匹配致使了頁面出錯。

一般狀況下,當咱們對HTML進行重大更改時,咱們還可能更改HTML對應的CSS結構,並更新JS以適應樣式和內容的更改。這些資源是相互依賴的,可是緩存的header是沒法描述這種依賴的。用戶最終看到的,多是一兩個新版本的資源,和其餘老的資源。

max-age和響應時間有關,所以,若是上述全部的資源都是在同一次訪問中請求的,他們大概會在同一時間到期,可是仍然有很小的可能發生競爭。若是你的某些頁面並不包含JS或者包含了不一樣的CSS,那麼過時時間可能就不一樣步了。更糟糕的是,更糟糕的是,瀏覽器老是從緩存中刪除東西,它不知道HTML、CSS和JS是相互依賴的,因此它會很高興地刪除一個而不是其餘的。上述的狀況,均可能會致使頁面資源的版本不匹配。

對用戶來講,他們最終會看到錯誤的頁面佈局和錯誤的頁面功能,從細微的錯誤到徹底不可用的內容。

謝天謝地,對用戶來講仍是有補救措施的。

  • 刷新可能會修復這個問題

若是頁面做爲刷新的一部分加載,瀏覽器會忽略max-age,向服務器進行驗證。所以,若是用戶遭遇了由於max-age而形成的錯誤,刷新是能夠解決問題的。固然,強迫用戶這樣作會下降信任度,由於這會讓你感受到你的網站是不靠譜的。

  • service worker可能會延長這些bug的壽命

假設你有如下的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

  • 緩存了script和style
  • 若是命中了緩存,就從緩存中取,不然發起網絡請求

若是咱們更改了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 & http緩存能夠同時使用,不要讓他們衝突

正如你所見,你能夠繞過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 & 可變內容 也能夠頗有效

對於可變內容使用max-age通常狀況下是一個錯誤的選擇,但也不老是這樣。好比,這個頁面設置了一個3分鐘的max-age. race condition在這個頁面是不會成爲問題的,由於這個頁面沒有任何遵循這一種模式的依賴(個人css,js,圖片等都遵循模式1-不可變內容),依賴於此頁的任何內容都不會遵循相同的模式。

這種模式意味着,若是我有幸寫了一篇熱門文章,個人cdn可讓個人服務器散熱,而我能忍受用戶須要花三分鐘時間纔看到文章更新。

這種模式不能隨便使用。若是我在文章中添加了一個新的部分,而且將這個部分連接到一篇新的文章,那麼我就創造了一個會爭用的依賴項。用戶能夠單擊連接,並在沒有引用部分的狀況下獲取文章的副本。若是我想避免這種狀況,我就得更新第一篇文章,刷新cdn, 等待3分鐘,而後在另外一篇文章中添加指向他的連接。是的…..你必須很是當心這種模式。

正確使用,緩存能極大的提升性能而且較少帶寬消耗。對於任何容易更改的URL,都支持不可變的內容,不然在服務器從新驗證時會使其安全。只有當你足夠勇敢,而且你確信你沒有可能會失去同步的依賴項時,再使用max-age和可變內容的模式。

相關文章
相關標籤/搜索