了解 Vary 標頭

已發表: 2022-03-10
快速總結↬ Vary HTTP 標頭每天以數十億的 HTTP 響應發送。 但它的使用從未實現其最初的願景,許多開發人員誤解了它的作用,甚至沒有意識到他們的 Web 服務器正在發送它。 隨著客戶端提示、變體和關鍵規範的出現,各種響應正在重新開始。

Vary HTTP 標頭每天以數十億的 HTTP 響應發送。 但它的使用從未實現其最初的願景,許多開發人員誤解了它的作用,甚至沒有意識到他們的 Web 服務器正在發送它。 隨著客戶端提示、變體和關鍵規範的出現,各種響應正在重新開始。

什麼是變化?

Vary 的故事始於一個關於網絡應該如何工作的美好想法。 原則上,URL 代表的不是網頁,而是概念資源,例如您的銀行對帳單。 想像一下,您想查看您的銀行對帳單:您訪問bank.com並為/statement發送一個GET請求。 到目前為止一切都很好,但是您沒有說明您想要聲明的格式。這就是為什麼您的瀏覽器還會在您的請求中包含諸如Accept: text/html之類的內容。 至少在理論上,這意味著您可以說Accept: text/csv並以不同的格式獲取相同的資源。

用戶與銀行之間的內容協商對話圖示
(查看大圖)

因為同一個 URL 現在會根據Accept標頭的值產生不同的響應,所以任何存儲此響應的緩存都需要知道該標頭很重要。 服務器告訴我們Accept標頭很重要,如下所示:

 Vary: Accept

您可以將其解讀為“此響應因您的請求的Accept標頭的值而異。”

這在今天的網絡上基本上行不通。 所謂的“內容協商”是個好主意,但失敗了。 但這並不意味著Vary沒有用。 您在網絡上訪問的頁面的相當一部分在響應中帶有Vary標頭 - 也許您的網站也有它們,而您不知道。 那麼,如果 header 不能用於內容協商,為什麼它仍然如此流行,瀏覽器又是如何處理的呢? 讓我們來看看。

我之前寫過關於內容交付網絡 (CDN) 的 Vary,這些中間緩存(例如 Fastly、CloudFront 和 Akamai)可以放在服務器和用戶之間。 瀏覽器也需要理解和響應 Vary 規則,而他們這樣做的方式與 CDN 處理 Vary 的方式不同。 在這篇文章中,我將探索瀏覽器中緩存變化的陰暗世界。

跳躍後更多! 繼續往下看↓

當今在瀏覽器中變化的用例

正如我們之前看到的,Vary 的傳統用途是使用AcceptAccept-LanguageAccept-Encoding標頭執行內容協商,而從歷史上看,其中前兩個已慘遭失敗。 改變Accept-Encoding以提供 Gzip 或 Brotli 壓縮的響應,在受支持的情況下,大多數情況下工作得相當好,但是現在所有的瀏覽器都支持 Gzip,所以這不是很令人興奮。

其中一些場景怎麼樣?

  • 我們希望提供與用戶屏幕寬度完全一致的圖像。 如果用戶調整他們的瀏覽器大小,我們將下載新圖像(根據客戶端提示而有所不同)。
  • 如果用戶註銷,我們希望避免使用他們登錄時緩存的任何頁面(使用 cookie 作為Key )。
  • 支持 WebP 圖片格式的瀏覽器用戶應該會得到 WebP 圖片; 否則,他們應該得到 JPEG。
  • 在高密度屏幕上使用瀏覽器時,用戶應該獲得 2x 圖像。 如果他們將瀏覽器窗口移動到標準密度屏幕上並刷新,他們應該得到 1x 圖像。

一路緩存

與充當所有用戶共享的巨大緩存的邊緣緩存不同,瀏覽器僅適用於一個用戶,但它具有許多不同的緩存用於不同的特定用途:

瀏覽器中的緩存圖示
(查看大圖)

其中一些是相當新的,並且準確了解正在從哪個緩存加載內容是一個複雜的計算,開發人員工具不能很好地支持。 以下是這些緩存的作用:

  • 圖像緩存
    這是一個頁面範圍的緩存,用於存儲解碼後的圖像數據,因此,例如,如果您在一個頁面上多次包含相同的圖像,瀏覽器只需要下載和解碼一次。
  • 預加載緩存
    這也是頁面範圍的,並存儲任何已預加載在Link標頭或<link rel="preload">標記中的內容,即使該資源通常是不可緩存的。 與圖像緩存一樣,當用戶離開頁面時,預加載緩存會被破壞。
  • 服務工作者緩存 API
    這提供了一個帶有可編程接口的緩存後端; 因此,除非您通過服務工作者中的 JavaScript 代碼專門將其放置在此處,否則此處不會存儲任何內容。 如果您在服務工作者fetch處理程序中明確這樣做,它也只會被檢查。 Service Worker 緩存是原始範圍的,雖然不能保證是持久的,但它比瀏覽器的 HTTP 緩存更持久。
  • HTTP 緩存
    這是人們最熟悉的主要緩存。 它是唯一關注 HTTP 級緩存頭(如Cache-Control )的緩存,並將這些與瀏覽器自己的啟發式規則相結合,以確定是否緩存某些內容以及緩存多長時間。 它的範圍最廣,被所有網站共享; 因此,如果兩個不相關的網站加載相同的資產(例如 Google Analytics),它們可能會共享相同的緩存命中。
  • HTTP/2 推送緩存(或“H2 推送緩存”)
    這與連接一起存在,它存儲已從服務器推送但尚未被使用該連接的任何頁面請求的對象。 它的作用域是使用特定連接的頁面,這與作用於單個源的作用基本相同,但是當連接關閉時它也會被銷毀。

其中,HTTP 緩存和 Service Worker 緩存是最好定義的。 至於圖像和預加載緩存,一些瀏覽器可能將它們實現為與特定導航的渲染相關的單個“內存緩存”,但我在這裡描述的心理模型仍然是思考這個過程的正確方法。 如果您有興趣,請參閱有關preload的規範說明。 在 H2 服務器推送的情況下,關於這個緩存的命運的討論仍然活躍。

請求在進入網絡之前檢查這些緩存的順序很重要,因為請求某些內容可能會將其從緩存的外層拉到內部緩存。 例如,如果您的 HTTP/2 服務器將樣式表與需要它的頁面一起推送,並且該頁面還使用<link rel="preload">標記預加載樣式表,那麼樣式表最終將觸及三個緩存在瀏覽器中。 首先,它將位於 H2 推送緩存中,等待被請求。 當瀏覽器渲染頁面並到達preload標記時,它將通過 HTTP 緩存(可能會存儲它,取決於樣式表的Cache-Control標頭)將樣式表從推送緩存中拉出,並將保存它在預加載緩存中。

HTTP/2 PUSH 流通過瀏覽器緩存
(查看大圖)

介紹 Vary 作為驗證者

好的,那麼當我們在這種情況下添加 Vary 時會發生什麼?

與中間緩存(例如 CDN)不同,瀏覽器通常不會實現為每個 URL 存儲多個變體的功能。 這樣做的理由是我們通常使用Vary的東西(主要是Accept-EncodingAccept-Language )在單個用戶的上下文中不會經常更改。 Accept-Encoding可能(但可能不會)在瀏覽器升級時發生變化,而Accept-Language很可能只有在您編輯操作系統的語言區域設置時才會發生變化。 以這種方式實現 Vary 也更容易,儘管一些規範作者認為這是一個錯誤。

大多數情況下,瀏覽器只存儲一個變體並不是什麼大損失,但重要的是,如果“varied on”數據確實發生了變化,我們不會意外地使用不再有效的變體。

折衷方案是將Vary視為驗證者,而不是密鑰。 瀏覽器以正常方式計算緩存鍵(本質上是使用 URL),然後如果它們獲得命中,它們會檢查請求是否滿足緩存響應中的任何 Vary 規則。 如果不是,則瀏覽器將請求視為緩存未命中,並繼續移動到下一層緩存或移出網絡。 當收到新的響應時,它將覆蓋緩存的版本,即使它在技術上是不同的變體。

展示不同的行為

為了演示Vary的處理方式,我製作了一個小測試套件。 該測試加載一系列不同的 URL,在不同的標頭上有所不同,並檢測請求是否已命中緩存。 我最初為此使用 ResourceTiming,但為了更好的兼容性,我最終切換到僅測量請求完成所需的時間(並故意為服務器端響應添加 1 秒延遲以使差異非常明顯)。

讓我們看看每種緩存類型以及Vary應該如何工作以及它是否真的像那樣工作。 對於每個測試,我在這裡展示我們是否應該期望從緩存中看到結果(“HIT”與“MISS”)以及實際發生的情況。

預載

目前僅在 Chrome 中支持預加載,其中預加載的響應存儲在內存緩存中,直到頁面需要它們。 如果響應是 HTTP 可緩存的,則響應還會在到達預加載緩存的途中填充 HTTP 緩存。 因為指定帶有預加載的請求標頭是不可能的,並且預加載緩存只持續與頁面一樣長,因此測試這很困難,但我們至少可以看到帶有Vary標頭的對象確實被成功預加載:

Google Chrome 中鏈接 rel=preload 的測試結果
(查看大圖)

服務工作者緩存 API

Chrome 和 Firefox 支持 Service Worker,在開發 Service Worker 規範時,作者希望修復他們認為瀏覽器中損壞的實現,使瀏覽器中的Vary更像 CDN。 這意味著雖然瀏覽器應該只在 HTTP 緩存中存儲一個變體,但它應該在 Cache API 中保存多個變體。 Firefox (54) 正確地做到了這一點,而 Chrome 使用與用於 HTTP 緩存的相同的可變驗證器邏輯(正在跟踪該錯誤)。

Google Chrome 中 Service Worker 緩存的測試結果
(查看大圖)

HTTP 緩存

主 HTTP 緩存應遵守Vary並在所有瀏覽器中始終如一地這樣做(作為驗證器)。 有關這方面的更多信息,請參閱 Mark Nottingham 的文章“瀏覽器緩存狀態,再訪”。

HTTP/2 推送緩存

應該觀察到Vary ,但實際上沒有瀏覽器真正尊重它,瀏覽器會很高興地匹配和消費推送的響應與響應不同的標頭中帶有隨機值的請求。

Google Chrome 中 H2 推送緩存的測試結果
(查看大圖)

“304(未修改)”皺紋

HTTP“304(未修改)”響應狀態令人著迷。 我們的“親愛的領導者”阿圖爾·伯格曼(Artur Bergman)向我指出了 HTTP 緩存規範中的這個寶石(強調我的):

生成 304 響應的服務器必須生成以下頭字段中的任何一個,這些頭字段將在對同一請求的 200(OK)響應中發送: Cache-ControlContent-LocationDateETagExpiresVary

為什麼304響應會返回Vary標頭? 當您閱讀到收到包含這些標頭的304響應時您應該做什麼時,情節會變厚:

如果選擇存儲響應進行更新,則緩存必須\[...] 使用 304(未修改)響應中提供的其他頭字段來替換存儲響應中相應頭字段的所有實例。

等等,什麼? 那麼,如果304Vary標頭與現有緩存對像中的不同,我們應該更新緩存對象嗎? 但這可能意味著它不再符合我們提出的要求!

在這種情況下,乍一看, 304似乎同時告訴您可以使用緩存版本,也不能使用緩存版本。 當然,如果服務器真的不想讓你使用緩存版本,它會發送200 ,而不是304 ; 因此,絕對應該使用緩存版本——但在對其應用更新後,它可能不會再次用於與最初實際填充緩存的請求相同的未來請求。

(旁注:在 Fastly,我們不尊重規範的這種怪癖。因此,如果我們從您的源服務器收到304 ,我們將繼續使用未修改的緩存對象,而不是重置 TTL。)

瀏覽器似乎確實尊重這一點,但有一個怪癖。 它們不僅更新響應標頭,還更新與之配對的請求標頭,以保證更新後緩存的響應與當前請求匹配。 這似乎是有道理的。 規範沒有提到這一點,因此瀏覽器供應商可以自由地做他們喜歡的事情; 幸運的是,所有瀏覽器都表現出相同的行為。

客戶提示

Google 的客戶端提示功能是 Vary 長期以來在瀏覽器中發生的最重要的新功能之一。 與Accept-EncodingAccept-Language不同,客戶端提示描述的值可能會隨著用戶在您的網站上移動而定期更改,具體如下:

  • DPR
    設備像素比,屏幕的像素密度(如果用戶有多個屏幕,可能會有所不同)
  • Save-Data
    用戶是否開啟了節流模式
  • Viewport-Width
    當前視口的像素寬度
  • Width
    以物理像素為單位的所需資源寬度

不僅對於單個用戶,這些值可能會發生變化,而且與寬度相關的值的範圍很大。 因此,我們完全可以對這些標頭使用Vary ,但我們冒著降低緩存效率甚至使緩存無效的風險。

關鍵標題提案

客戶端提示和其他高度細化的標頭適用於 Mark 一直在研究的名為 Key 的提案。 讓我們看幾個例子:

 Key: Viewport-Width;div=50

這表示響應會​​根據Viewport-Width請求標頭的值而變化,但會向下舍入到最接近的 50 像素的倍數!

 Key: cookie;param=sessionAuth;param=flags

將此標頭添加到響應中意味著我們在兩個特定的 cookie 上有所不同: sessionAuthflags 。 如果它們沒有改變,我們可以將此響應重用於未來的請求。

因此, KeyVary之間的主要區別是:

  • Key允許對標頭中的子字段進行更改,這突然使更改 cookie 變得可行,因為您只能更改一個 cookie — 這將是巨大的;
  • 可以將單個值存儲到範圍中,以增加緩存命中的機會,這對於改變諸如視口寬度之類的東西特別有用。
  • 具有相同 URL 的所有變體必須具有相同的鍵。 因此,如果緩存接收到一個新響應的 URL 已經有一些現有變體,並且新響應的Key標頭值與那些現有變體上的值不匹配,則必須從緩存中逐出所有變體。

在撰寫本文時,沒有瀏覽器或 CDN 支持Key ,儘管在某些 CDN 中,您可以通過將傳入的標頭拆分為多個私有標頭並對其進行更改來獲得相同的效果(請參閱我們的帖子,“Getting the Most Out of Vary With Fastly”),因此瀏覽器是Key可以產生影響的主要領域。

所有變體都具有相同的關鍵配方的要求有些限制,我希望在規範中看到某種“提前退出”選項。 這將使您能夠執行諸如“改變身份驗證狀態,如果登錄,也改變偏好”之類的事情。

變體提案

Key是一個很好的通用機制,但是一些頭部的值有更複雜的規則,理解這些值的語義可以幫助我們找到減少緩存變化的自動化方法。 例如,假設有兩個請求帶有不同Accept-Languageen-gben-us ,但是儘管您的網站確實支持語言變化,但您只有一個“英語”。 如果我們回答美國英語的請求並且該響應被緩存在 CDN 上,那麼它不能被重用於英國英語請求,因為Accept-Language值會不同並且緩存不夠聰明,無法更好地了解.

大張旗鼓地輸入變體提案。 這將使服務器能夠描述它們支持的變體,允許緩存做出更明智的決定,即哪些變體實際上是不同的,哪些實際上是相同的。

目前,Variants 是一個非常早期的草案,因為它旨在幫助Accept-EncodingAccept-Language ,它的用處僅限於共享緩存,例如 CDN,而不是瀏覽器緩存。 但它很好地與Key配對並完成了圖片以更好地控制緩存變化。

結論

這裡有很多東西可以吸收,雖然了解瀏覽器如何在後台工作可能很有趣,但您也可以從中提取一些簡單的東西:

  • 大多數瀏覽器將Vary視為驗證器。 如果您希望緩存多個單獨的變體,請找到一種使用不同 URL 的方法。
  • 對於使用 HTTP/2 服務器推送推送的資源,瀏覽器會忽略Vary ,因此不要對推送的任何內容進行更改。
  • 瀏覽器有大量的緩存,它們以不同的方式工作。 值得嘗試了解您的緩存決策如何影響每一項的性能,尤其是在Vary的上下文中。
  • Vary並沒有它應有的用處,而Key與 Client Hints 配對開始改變這一點。 跟隨瀏覽器支持了解何時可以開始使用它們。

繼續前進,多變。