關於四種快取的故事
談到 preload,HTTP/2 push 以及 Service workers,人們都有很多見解,但也有很多困惑。 所以,我想給你講述一個關於 HTTP 請求履行自己使命,為了匹配資源而旅行的故事。
這個故事基於 Chromium 的術語與概念,在其他瀏覽器可能會有所不同。
複製程式碼
Questy 的旅行
Questy 是一個請求。它由渲染引擎(簡稱 renderer)在內部建立,它強烈渴望找到一個能夠讓它完成使命並且一直(至少到由於標籤被關閉導致的文件被分離的時候)快樂地在一起的資源。
所以 Questy 啟程去追尋幸福。但它會在哪找到適合它的資源呢? 最近的地方是...
記憶體快取
記憶體快取有一個充滿資源的大容器。它包含了 renderer 獲取的當前文件的所有資源,並且會在文件的生命週期內儲存。這就意味著 Questy 尋找的資源如果已經在當前文件的其他地方被獲取過,那麼這個資源將會在記憶體快取中被找到。 不過稱它為「短期記憶體快取」或許更合適,因為記憶體快取只在導航結束前儲存資源,在某些情況下還可能更短。
出現 Questy 尋找的資源已經被獲取過這一情況有很多潛在原因。
preloader 可能最大的一個。如果 Questy 是作為 DOM 節點被 HTML 解析器創造出來的話,那麼在 preloader 的 HTML 標記化階段中它所需要的資源很有可能被獲取了。
顯式的 preload 的指令(<link rel=preload>
) 是另外一種預載入的資源已經被儲存在記憶體快取中的情況。
另外,先前的 DOM 節點或者 CSS 規則也可能已經獲取了相同的資源。例如,一個頁面包含多個有相同 src
屬性的 <img>
元素,這時只會獲取一個資源。這種能夠讓多個元素獲取一個資源的機制就是由於記憶體快取的存在。
但是記憶體快取並不會輕易讓請求匹配到資源。顯然,為了讓請求和資源匹配,他們有匹配的 URL 還不夠,必須還有匹配的資源型別,CORS 模式和一些其他特性。
來自記憶體快取的請求的匹配特徵在規範中並沒有很好的定義,因此在瀏覽器實現中可能會略有不同。
複製程式碼
記憶體快取也不關心 HTTP 語義。就算儲存的資源有 max-age=0
或 no-cache
Cache-Control
訊息頭,那也不是記憶體快取關心的東西。由於它允許在當前導航中重用資源,所以 HTTP 語義在這裡並不重要。
唯一的例外是 no-store
指令,記憶體快取在某些情況下會遵守該指令(例如,當資源被單獨的節點重用時)。
Questy 繼續向記憶體快取尋求匹配的資源。不過一個也沒找到。
但 Questy 並沒有放棄。它通過了 Resource Timing 和 DevTools network 註冊點,在那裡註冊為尋找資源的請求(這意味著它現在將顯示在 DevTools 以及 resource timing 中,假定它最終會找到它的資源)。
在註冊這一行政部分完成後,它繼續朝著...
Service Worker 快取
與記憶體快取不同,Service Worker 快取並不遵循任何傳統規則。它只遵守他們的主人(即 Web 開發者)告訴它的規則。因此在某種程度上它是不可預測的。
首先,只有當頁面安裝了 Service Worker,它才會存在。而且由於它的邏輯不是內建於瀏覽器,而是由 Web 開發者通過 JavaScript 定義的,Questy 並不知道它是否願意為自己尋找資源,即便它願意,這資源就是它所夢寐以求的嗎?它會是儲存在它的快取中的匹配資源嗎?還是隻是由 Service Worker 的主人的扭曲邏輯所建立的一個響應?
沒有人知道。因為 Service Workers 擁有自己的邏輯,所以他們可以任意地完成匹配請求和潛在資源、包裝 Response 物件中這些行為。
Service Worker 有一個使它能夠保留資源的 Cache API。它和記憶體快取之間的一個主要區別是它是持久的。即使選項卡關閉或瀏覽器重新啟動,儲存在該快取中的資源仍會保留。他們會從快取中被逐出的一種情況是,開發者明確將他們逐出(使用cache.delete(resource)
)。另一種情況是,瀏覽器用完了儲存空間,在這種情況下,整個 Service Worker 快取會與所有其他原始儲存,如indexedDB、localStorage 等,一起被刪除。這樣,Service Worker 就保持在它快取中的資源與它自身以及其他原始儲存之間同步。
Service Worker 負責最多一個主機的範圍。因此 Service Worker 只能對該範圍內的文件請求進行響應。
Questy 找到了 Service Worker 並問它是否有資源。但 Service Worker 從來沒有在自己掌管範圍內的看到它要的資源,所以沒有相應的資源給予。因此,Service Worker 派遣(使用fetch()
) Questy 繼續在網路堆疊的未知大陸上搜尋資源。
而在網路堆疊中,尋找資源的最好地方就是...
HTTP 快取
HTTP 快取,有時也被它的快取朋友稱為「磁碟快取」,它與 Questy 之前看到的快取完全不同。
一方面,它是持久的,允許資源在會話之間甚至跨站點重用。如果某個資源由一個站點快取,那麼 HTTP 快取也允許其他站點重用該資源。
同時,HTTP 快取遵循 HTTP 語義(它的名字就表明了這一點)。它樂於為其認為「新鮮」的資源提供服務(基於快取生命週期,由其響應的快取頭指示),重新驗證資源,並拒絕儲存不該儲存的資源。
它是一個持久快取,所以它也需要驅逐資源,但與 Service Worker 快取不同的是,只要快取需要空間去儲存更重要或更流行的資源時,資源就能一個接一個地被逐出。
HTTP 快取具有一個基於記憶體的元件。在元件中,它會對進入的請求進行資源匹配。但當它找到了匹配的資源時,它會從磁碟中獲取資源內容,而這會是一個昂貴的操作。
我們之前提到過,HTTP 快取尊重HTTP語義。這個說法幾乎完全正確,但有一個例外:HTTP 快取會在有限的時間記憶體儲資源。
通過顯式的提示(`<link rel=prefetch>`)或者瀏覽器的內部策略,能為下一個導航預獲取資源,而那些預獲取的資源會儲存到下次導航,即使它們是不可快取的。
所以當這種預獲取的資源到達 HTTP 快取時,它會被快取(並無需重新驗證)5分鐘。
複製程式碼
HTTP快取看起來相當嚴格,但 Questy 還是鼓起勇氣問它是否有匹配的資源。答案是沒有。
它將不得不繼續走向網路。通過網路的旅程是可怕且不可預知的,但 Questy 明白它無論如何都必須找到它的資源。所以它繼續前進。它發現了一個 HTTP/2 會話,接著很快就會通過網路傳送,這時它突然看到了......
Push 「Cache」
Push 快取(稱為「unclaimed push streams container」或許更合適,但不那麼容易上手)是儲存 HTTP/2 推送資源的地方。它們作為 HTTP/2 會話的一部分進行儲存,並具有多種含義。
該容器沒有任何永續性。如果會話被終止,那麼沒有被請求的所有資源都會消失。如果使用不同的 HTTP/2 會話獲取資源,它將不會匹配。最重要的是,資源只在有限的時間內儲存在 push 快取容器中。(在基於 Chromium 的瀏覽器中約5分鐘)
push 快取根據其URL以及其各種請求頭匹配請求與資源,但它不適用嚴格的HTTP語義。
push 快取在規範中也沒有很好的定義,實現可能因瀏覽器、作業系統和其他 HTTP/2 客戶端而異。
複製程式碼
Questy 沒報太大希望,但它仍然詢問 Push 快取是否有匹配它的資源。而令人驚訝的是,它有資源!Questy 非常開心地接受了資源(這意味著它從無人認領的容器中刪除了 HTTP/2 流)。現在它可以帶著資源回到 renderer 中去了。
在他們返回的路上,被 HTTP 快取滯留,HTTP 快取拿了一份資源的拷貝儲存著,以防將來的請求需要它。
當他們離開網路堆疊返回到 Service Worker 中時,Service Worker 也儲存了一份資源拷貝,之後再送他們回到 renderer。
終於,他們回到了 renderer,記憶體快取保留了資源的引用(不是拷貝),以便在這個導航會話中為將來的請求分配相同的資源。
他們從此過著幸福快樂的生活。直到文件被分離,他們都會去見垃圾回收器。
但那是另一天的故事了。
結論
那麼,我們能從 Questy 的旅程中學到什麼?
- 不同的請求可以通過瀏覽器的不同快取中的資源進行匹配。
- 與請求資源所匹配的快取可能會影響 DevTools 和 Resource Timing 中顯示的方式。
- 推送的資源不會永久儲存,除非它們的流被請求接受。
- 不可快取的預載入資源將不會用於下一個導航。這是 preload 和 preftech 的主要區別之一。
- 這裡邊還有許多不明確的地方,觀察到行為可能也會因瀏覽器實現而有所不同。我們需要解決這個問題。
總而言之,如果你使用 preload、H2 push、Service Worker 或其他先進技術來嘗試加速你的網站時,你可能會注意到內部快取實現的情況。通過了解這些內部快取以及它們的執行方式可能會幫助你更好地理解網站現狀,並有可能避免不必要問題。