Cache-Control for Civilians

一介文夫發表於2019-04-01

這是一篇譯文,原文在這裡,英文好的可以去閱讀原文,已獲得作者同意翻譯本文。

文章目錄

  1. Cache-Control

  2. public and private

  3. max-age

    a. s-maxage

  4. no-store

  5. no-cache

  6. must-revalidate

    a. proxy-revalidate

  7. immutable

  8. stale-while-revalidate

  9. stale-if-error

  10. 快取重新整理

    a. 沒有快取重新整理 – style.css

    b. 請求引數 – style.css?v=1.2.14

    c. 檔名唯一 – style.ae3f66.css

    d. Clear-Site-Data

  11. 例子和一些用法

    a. 線上的銀行頁面

    b. 實時列車時刻表頁面

    c. FAQ頁面

    d. 靜態的js/css

    e. 裝飾的圖片

  12. 需要記住的

  13. 相關資源

    a. 按我說的做,別按我做的去做

最好的網路請求就是永遠都不會發生的請求: 想要獲得更快的網站時,避免網路請求會遠勝於有網路。因此,擁有一個可靠的快取策略可以讓你的使用者感覺非常不一樣。

話雖如此,但在我的工作中,越來越多的時候我看到是不考慮甚至完全忽視快取實踐。也許是因為第一次看見時沒有過度關注,或者可能是因為缺乏這樣的意識或知識?不管怎樣,讓我們來複習一下。

Cache-Control

一個最普遍、有效的管理資源快取的方法是通過HTTP的頭部欄位Cache-Control,這個頭部可以應用到每一個網站的資源,這意味著我們頁面上的所有內容都可以有一個非常定製和精細的快取策略。這也使得其非常複雜和強大。

一個Cache-Control頭部可能看起來像這樣:

Cache-Control: public, max-age=31536000
複製程式碼

Cache-Control是欄位名,publicmax-age=31536000是指令。Cache-Control可以接受多個指令,在這篇部落格中我會介紹這些指令以及它們的用例

public and private

public意味著任何可以快取的地方都會快取一份響應的資料。包括CDN,代理伺服器等。public通常都是多餘的,因為其他的指令(比如max-age)已經隱含它了。

相反,private指令只允許在相應的接受方(客戶端或瀏覽器)儲存一份。雖然private不是一個安全相關的指令,但是它可以防止公共快取(比如CDN)快取一些使用者相關的資訊

max-age

max-age 定義了一段時間(相對於請求時)來告訴瀏覽器相應是否需要重新整理

Cache-Control: max-age=60
複製程式碼

這條指令會告訴瀏覽器,它可以在接下來的60秒內使用快取中的這個檔案,而不必重新驗證它。60秒後,瀏覽器將伺服器發請求重新驗證檔案。

如果伺服器有一個新檔案供瀏覽器下載,它會返回狀態碼200,下載新檔案,並替換掉HTTP快取中舊檔案,並按照指令快取檔案。

如果伺服器沒有需要下載的更新副本,伺服器會以304響應,並更新快取的時間。這意味著,如果Cache-Control: max-age=60仍然存在,快取檔案還將快取60秒。總共快取120秒。

注意: 如果單單使用max-age會有一個非常大的風險…max-age可以告訴瀏覽器資源是否已經過時,但是它不能告訴瀏覽器它是否可以使用過時的版本。瀏覽器可能在沒有驗證的情況下使用過期的版本。這種行為有些不確定性,導致我們很難判斷瀏覽器究竟做了什麼。幸好,我們有一系列更明確的指令,我們可以用這些指令來增強max-age。感謝Andy Davies的幫助

s-maxage

The s-maxage (note the absence of the - between max and age) will take precedence over the max-age directive but only in the context of shared caches. Using max-age and s-maxage in conjunction allows you to have different fresh durations for private and public caches (e.g. proxies, CDNs) respectively.

s-maxage (注意沒有"-"在maxage之間)將優先於max-age指令,但僅在共享快取的情況下有效。結合使用max-ages-maxage,你可以分別對私有快取和公共快取(例如代理、CDN)擁有不同的快取重新整理時間。

no-store

Cache-Control: no-store
複製程式碼

如果我們不想快取檔案怎麼辦?如果檔案包含敏感資訊怎麼辦?也許這是一個包含你銀行詳細資訊的HTML頁面?又或者資訊是實時的?也又可能是一個包含實時股票價格的頁面?我們肯定不想快取或從快取中獲取這樣的資訊: 我們總是希望清除敏感的資訊,然後獲取最新的實時資訊。現在我們需要用no-store

no-store是一條非常強大的指令,告訴瀏覽器或其他裝置不要快取任何資訊。任何帶有這條指令的資源,無論在什麼情況下都會發起請求。

no-cache

Cache-Control: no-cache
複製程式碼

這個是讓大多數人困惑的。no-cache並不意味著沒有快取。它的意思是在你向伺服器重新驗證快取副本,並且伺服器表示你可以使用快取副本之前,不要使用快取。是的,這聽起來應該叫做必須重新驗證!只是聽起來也不是這樣。

no-cache實際上是一種非常聰明的方式,可以保證內容始終是最新的,但是如果可能的話,也可以使用更快的快取策略。no-cache總是會發起網路請求,因為在返回瀏覽器的快取之前,它必須與伺服器重新驗證(除非伺服器有更新的響應內容),但是如果伺服器的響應良好,網路傳輸只是檔案的頭部:可以從快取中抓取主體,而不是重新載入。

所以,就像我說的,這是一種將獲取最新資源和從快取中獲取檔案的可能性結合起來的聰明方法,但是它會發出網路請求,至少會有HTTP頭響應。

一個好的no-cache用例幾乎是任何動態HTML頁面。想想一個網站的首頁:它不是實時的,也不包含任何敏感資訊,但是理想情況下,我們希望頁面總是顯示最新的內容。我們可以使用cache-control: no-cache來指示瀏覽器首先檢查伺服器,如果伺服器沒有更新的內容( 304 ),我們就使用快取版本。如果伺服器確實有一些更新的內容,它會照原樣做出響應( 200 )併傳送更新的檔案。

提示:在傳送no-cache指令的同時傳送max-age指令是沒有用的,因為重新驗證的時間限制是零秒。

must-revalidate

更令人困惑的是,雖然上面的那條指令聽起來應該被稱為must-revalidate,但事實上must-revalidate是不同的(但相似)。

Cache-Control: must-revalidate, max-age=600
複製程式碼

must-revalidate需要相關的max-age指令;上面程式碼中,我們把它設定為十分鐘。

其中no-cache將立即與伺服器進行重新驗證,並且只有在伺服器認為可以的情況下才使用快取,must-revalidate就像有寬限期的no-cache一樣。在最初的十分鐘裡,瀏覽器不會發請求去伺服器重新驗證,但是十分鐘過後,就會發請求去伺服器驗證了。如果伺服器說沒有什麼新的,它會以304作為響應,並且新的快取控制頭會應用到快取中—十分鐘後才會再次傳送請求。如果十分鐘後,伺服器上有一個更新的檔案,我們會得到一個200的響應和它的內容,並且本地快取會得到更新。

must-revalidate的一個很好的例子就是像我這樣的部落格:有很少改變的靜態頁面。當然,最新的內容是可取的,但是考慮到我的網站變化的頻率,我們不需要像no-cache一樣頻繁的檢測更新。十分鐘驗證一次足夠了。

proxy-revalidate

與s-maxage類似,proxy-revalidatemust-revalidate的一個只對公共快取起作用的特定版本。

immutable

immutable一個非常新且簡潔的指令,它告訴瀏覽器更多關於我們傳送的檔案的型別——它的內容是可變的還是不可變的?但是,在我們看immutable怎麼用之前,讓我們先看看它正在解決的問題:

使用者重新整理會導致瀏覽器重新驗證檔案,而不管檔案是否是最新的,因為使用者重新整理通常意味著以下兩種情況之一:

  1. 頁面不能工作了,或者
  2. 內容過時了

那麼讓我們檢查一下伺服器上是否還有更新的東西。

如果伺服器上有更新的檔案,我們肯定想下載它。因此,我們將得到200個響應、一個新檔案,並且——希望——問題得到解決。然而,如果伺服器上沒有新檔案,我們將返回一個304頭部,沒有新檔案,而是一個完整的往返延遲。如果我們重新驗證許多檔案,導致很多的304,這可能會增加數百毫秒的不必要的開銷。

immutable是一種告訴瀏覽器檔案永遠不會改變的方式,它是不可變的,因此永遠不需要重新驗證它。我們可以完全減少往返延遲的開銷。我們所說的可變或不可變檔案是什麼意思呢?

- style.css: 當我們改變這個檔案的內容時,我們根本不改變它的名字。檔案總是存在的,它的內容總是變化的。這個檔案是可變的。 
- style.ae3f66.css: 這個檔案是獨一無二的——它是以基於內容的指紋命名的,所以當內容改變的時候,我們得到了一個全新的檔案。這個檔案是不可變的。
複製程式碼

我們將在快取重新整理部分詳細討論這一點。

如果我們能夠以某種方式向瀏覽器傳達我們的檔案是不可變的——它的內容永遠不會改變——那麼我們也可以讓瀏覽器知道,它不需要費心去檢查更新的版本:永遠不會有更新的版本,因為當檔案的內容改變時,它就不再存在了。

這正是不可變指令的作用:

Cache-Control: max-age=31536000, immutable
複製程式碼

在支援immutable的瀏覽器中,使用者重新整理永遠不會在31536000秒內導致重新驗證。這意味著不會花費不必要的往返時間在304響應上,這可能會在關鍵路徑上為我們節省大量延遲( CSS塊渲染)。在高延遲連線上,這種節省可能是顯而易見的。

注意: 你不應該把immutable應用到任何不是不可變的檔案。您還應該有一個非常強大的快取重新整理策略,這樣您就不會無意中主動快取一個應用了不可變的檔案。

stale-while-revalidate

我真的,真的希望stale-while-revalidate能得到有更好的支援。

到目前為止,我們已經談了很多關於重新驗證的事情: 瀏覽器發請求到伺服器檢查是否有檔案更新。在高延遲連線上,僅重新驗證的持續時間就已很明顯,並且該時間就是死時間——在我們從伺服器得到響應之前,我們既不能釋放快取(304),也不能下載新檔案(200)。

stale-while-revalidate提供了一個寬限期(由我們定義),在寬限期內,當我們檢查更新版本時,允許瀏覽器使用過期資源。

Cache-Control: max-age=31536000, stale-while-revalidate=86400
複製程式碼

這條指令告訴瀏覽器,“這個檔案可以使用一年,但是在那一年結束後,你有一個額外的星期可以繼續使用這個過時的資源,同時在後臺重新驗證它”。

對非關鍵資源來說,stale-while-revalidate是一條很好的指令,當然,我們想要最新的版本,但是我們知道,如果我們在檢查更新時使用過期的資源,不會造成任何損害。

stale-if-error

以類似於tale-while-revalidate的方式,stale-if-error允許瀏覽器有一個寬限期,在該寬限期內,如果重新驗證的資源返回5xx類錯誤,你可以返回一個過期的資源。

Cache-Control: max-age=2419200, stale-if-error=86400
複製程式碼

這條指令,我們指示快取檔案在28天(2419200秒)內是新的,並且如果在此之後遇到錯誤,我們將允許額外的一天(86400秒),在此期間我們返回過期資源。

Cache Busting

談論快取而不談論快取重新整理是不負責任的。我總是建議在考慮快取策略之前先解決快取重新整理策略。反之則非常頭痛。

快取重新整理是為了解決這樣一個問題: 我剛剛告訴瀏覽器在下一年裡使用這個檔案,但是我剛剛改變了它,我不想讓使用者等一整年後才得到新的檔案!我該怎麼能干預呢?!

沒有快取重新整理 – style.css

這是最不可取的做法: 這是絕對不會刷行快取的。這是一個可變檔案,我們很難快取它。

您應該非常小心地快取這樣的檔案,因為一旦它們出現在使用者的裝置上,我們幾乎就失去了對它們的所有控制。

儘管這個例子是一個樣式表,但是HTML頁面也完全屬於這個陣營。我們不能改變網頁的檔名——想象一下這會造成多大的影響!—這就是為什麼我們傾向於根本不快取它們。

請求引數 – style.css?v=1.2.14

這裡,我們仍然有一個可變的檔案,但是我們在它的檔案路徑中新增了一個查詢字串。雖然比什麼都不加要好,但它仍然不完美。如果在中間的什麼步驟去掉了查詢字串,我們就可以回到之前的情況,即完全沒有快取重新整理。許多代理伺服器和CDN不會快取帶有字串的任何內容(例如,從Cloudflare自己的文件就說過對"style.css?something"的請求會被替換為"style.css"),可能是配置,也可能是處於防禦性地目的(查詢字串可能包含特定於一個特定響應的資訊)。

檔名唯一 – style.ae3f66.css

指紋識別是快取重新整理檔案的首選方法。通過每次內容改變時都改變檔名,我們在技術上不會快取任何東西:我們最終會得到一個全新的檔案!這非常健壯,並且允許使用不可變的。如果您可以在靜態資產上實現這一點,請這樣做!一旦您成功實現了這種非常可靠的快取重新整理策略,您就可以使用最激進的快取形式:

Cache-Control: max-age=31536000, immutable
複製程式碼

實現細節

這個方法的關鍵是改變檔名,但它不一定是指紋。以下所有示例都具有相同的效果:

  1. /assets/style.ae3f66.css: 通過hash檔案來重新整理.
  2. /assets/style.1.2.14.css: 通過版本號來重新整理.
  3. /assets/1.2.14/style.css: 通過目錄結構來重新整理.

然而,最後一個例子意味著我們對每個版本進行版本控制,而不是對每個單獨的檔案進行版本控制。這反過來意味著,如果我們只需要快取我們的樣式表,我們還必須快取該版本的所有靜態檔案。這是潛在浪費,所以最好選擇(1)或(2)。

Clear-Site-Data

快取失效很難——眾所周知如此——所以目前有一個規範正在幫助開發人員非常明確地一次性清除他們站點的整個快取:Clear-Site-Data

我不想在這篇文章中講太多細節,因為Clear-Site-Data不是快取控制指令,而是一個全新的HTTP頭。

Clear-Site-Data: "cache"
複製程式碼

將該頭應用到您的任何一個源資產將清除整個源的快取,而不僅僅是它所附加的檔案。這意味著,如果你需要從所有訪問者的快取中清除你的整個站點,你可以只將上面的標題應用到你的網頁上。

在撰寫本文時,瀏覽器支援僅Chrome, Android Webview, Firefox, 和 Opera。

提示: Clear-Site-Data將接受許多指令:"cookies"、"storage"、"executionContexts"和"*"(表示所有上述內容)。

例子和一些用法

好吧,讓我們來看一些可能使用Cache-Control頭部的場景。

線上的銀行頁面

像網上銀行應用程式頁面,列出你最近的交易,當前的餘額,也許還有些敏感的銀行賬戶資訊,需要是最新的(想象一下,你收到了一個列出你一週前餘額的頁面!)並確保不被他人看見(您肯定不希望您的銀行詳細資訊儲存在共享快取(或者任何快取,真的) )。

為此,我們可以使用:

Request URL: /account/
Cache-Control: no-store
複製程式碼

根據規範,這足以防止瀏覽器在私有和共享快取中長久的儲存響應:

no-store告訴快取不準儲存請求或響應的任何部分。該指令適用於私有和共享快取。"不準"在這裡意味著快取不得有意將資訊儲存在非易失性儲存器中,並且必須盡最大努力在轉發資訊後儘快將其從易失性儲存器中移除。

但是如果你想有更強的防禦性,也許你可以選擇:

Request URL: /account/
Cache-Control: private, no-cache, no-store
複製程式碼

這將明確指示不要在公共快取(例如CDN )中儲存任何內容,要始終提供儘可能最新的副本,並且不要將任何內容儲存到儲存中。

實時列車時刻表頁面

如果我們正在構建一個顯示近實時資訊的頁面,如果這些資訊存在的話,我們希望確保使用者總是能看到我們能給他們的最好的、最新的資訊。我們可以使用:

Request URL: /live-updates/
Cache-Control: no-cache
複製程式碼

這個簡單的指令意味著瀏覽器在與伺服器確認快取是否被允許使用前不會直接從快取中顯示響應。這意味著使用者永遠不會看到過時的列車資訊,但是如果伺服器要求快取映象最新的資訊,他們就可以從從快取中獲取。

對於幾乎所有網頁來說,這通常是一個明智的預設設定:給我們最新的可能內容,但是如果可能的話,讓我們使用快取來加速。

FAQ頁面

像FAQ這樣的頁面很可能很少更新,而且其中的內容也不太可能對時間敏感。它當然沒有實時運動成績或飛行狀態那麼重要。我們可能可以快取這樣的頁面一段時間,並強制瀏覽器定期檢查新內容,而不是每次訪問。讓我們看看:

Request URL: /faqs/
Cache-Control: max-age=604800, must-revalidate
複製程式碼

這告訴瀏覽器將網頁快取一週( 604800秒),一旦這一週結束,我們就需要向伺服器查詢是否需要更新。

注意:在同一個網站中對不同的頁面採用不同的快取策略可能會導致這樣一個問題: 您的 no-cache 主頁請求它引用的最新樣式,f4fa2b.css,但是您的要快取三天的FAQ頁面仍然指向樣式a3f66.css。這可能會有輕微的影響,但這是一個您應該注意的場景。

靜態的js/css

假設我們的app.[fingerprint].js檔案更新非常頻繁——可能在我們釋出的每一個版本中都是如此——但是我們也在每次檔案發生變化時都會根據內容對其重新命名(這非常好),然後我們可以這樣做:

Request URL: /static/app.1be87a.js
Cache-Control: max-age=31536000, immutable
複製程式碼

我們是否非常頻繁地更新JS並不重要:因為我們有可靠地快取重新整理,所以我們可以想快取多久就快取多久。在這種情況下,我們選擇快取一年。我選擇了一年,因為首先,一年是很長的時間,其次,瀏覽器實際上不太可能將檔案儲存那麼長時間(瀏覽器有有限的儲存空間可以用於HTTP快取,所以它們自己會定期清空部分檔案;使用者可能也會清除他們快取)。任何超過一年的設定不會更有效。

此外,因為這個檔案的內容從不改變,我們可以讓瀏覽器知道這個檔案是不可變的。即使使用者重新整理了頁面,我們也不需要在一年內對它進行重新驗證。我們不僅獲得了使用快取的速度優勢,還避免了重新驗證的延遲損失。

裝飾的圖片

想象一張純粹裝飾性的照片伴隨著一篇文章。它不是資訊圖或圖表,它不包含任何對理解頁面其餘部分至關重要的內容,使用者甚至不會真正注意到它是否完全丟失了。

影象通常是需要下載的非常重的資源,所以我們想快取它;它對頁面並不重要,所以我們不需要獲取最新版本;我們甚至有可能在影象有點過時後就不再提供影象了。讓我們這樣做:

Request URL: /content/masthead.jpg
Cache-Control: max-age=2419200, must-revalidate, stale-while-revalidate=86400
複製程式碼

在這裡,我們告訴瀏覽器將影象儲存28天( 2419200秒),我們要在28天的時間限制後向伺服器查詢更新,如果影象過期不到一週( 86400秒),我們在後臺獲取最新版本時使用該影象。

需要記住的

  • 快取重新整理至關重要。在開始快取策略之前,先制定快取重新整理策略。
  • 一般來說,快取網頁內容是個壞主意。超文字連結不能被破壞,而且由於你的網頁通常是進入你網頁其餘子資源的入口,你最終也會快取對你靜態資產的引用。這會讓你(和你的使用者)感到沮喪。
  • 如果您要快取任何一個網頁,如果一個網站上不同型別的網頁上有不同的快取策略,並且其中一類網頁總是新的,而另一類網頁有時是從快取中獲取的,那麼這可能會導致不一致。
  • 如果你能依賴快取重新整理去快取你的靜態資源,那麼你最好用一個不可變的指令一次性快取多年。
  • 非關鍵內容可以有一個過時的寬限期,指令類似於過期同時重新驗證。
  • immutablestale-while-revalidate不僅給我們帶來了快取的傳統優勢,還允許我們在重新驗證時降低延遲成本。

儘量避免網路請求,可以讓我們的使用者獲得更快的體驗(同時降低基礎設施的吞吐量)。通過對我們的資源有一個好的看法,並對我們可用的東西有一個概述,我們可以開始為我們自己的應用程式設計非常精細、定製和有效的快取策略。

用快取管理一切

相關資源

按我說的做,別按我做的去做,(作者調皮了)

在別人在Hacker News上噴我虛偽之前,我想說的是我自己的快取策略非常不好,我甚至都不打算去做。(全文完)

第一次翻譯這麼長的,如有錯誤,請指出,一定修改。