原文地址:A Tale of Four Caches瀏覽器
談到 preload,HTTP/2 push 以及 Service workers,人們都有不少看法,但也有不少困惑。 因此,我想給你講述一個關於 HTTP 請求履行本身使命,爲了匹配資源而旅行的故事。緩存
這個故事基於 Chromium 的術語與概念,在其餘瀏覽器可能會有所不一樣。
複製代碼
Questy 是一個請求。它由渲染引擎(簡稱 renderer)在內部建立,它強烈渴望找到一個可以讓它完成使命而且一直(至少到因爲標籤被關閉致使的文檔被分離的時候)快樂地在一塊兒的資源。bash
因此 Questy 啓程去追尋幸福。但它會在哪找到適合它的資源呢? 最近的地方是...網絡
內存緩存有一個充滿資源的大容器。它包含了 renderer 獲取的當前文檔的全部資源,而且會在文檔的生命週期內保存。這就意味着 Questy 尋找的資源若是已經在當前文檔的其餘地方被獲取過,那麼這個資源將會在內存緩存中被找到。 不過稱它爲「短時間內存緩存」或許更合適,由於內存緩存只在導航結束前保存資源,在某些狀況下還可能更短。fetch
出現 Questy 尋找的資源已經被獲取過這一狀況有不少潛在緣由。網站
preloader 可能最大的一個。若是 Questy 是做爲 DOM 節點被 HTML 解析器創造出來的話,那麼在 preloader 的 HTML 標記化階段中它所須要的資源頗有可能被獲取了。spa
顯式的 preload 的指令(<link rel=preload>
) 是另一種預加載的資源已經被存儲在內存緩存中的狀況。操作系統
另外,先前的 DOM 節點或者 CSS 規則也可能已經獲取了相同的資源。例如,一個頁面包含多個有相同 src
屬性的 <img>
元素,這時只會獲取一個資源。這種可以讓多個元素獲取一個資源的機制就是因爲內存緩存的存在。.net
可是內存緩存並不會輕易讓請求匹配到資源。顯然,爲了讓請求和資源匹配,他們有匹配的 URL 還不夠,必須還有匹配的資源類型,CORS 模式和一些其餘特性。3d
來自內存緩存的請求的匹配特徵在規範中並無很好的定義,所以在瀏覽器實現中可能會略有不一樣。
複製代碼
內存緩存也不關心 HTTP 語義。就算存儲的資源有 max-age=0
或 no-cache
Cache-Control
消息頭,那也不是內存緩存關心的東西。因爲它容許在當前導航中重用資源,因此 HTTP 語義在這裏並不重要。
惟一的例外是 no-store
指令,內存緩存在某些狀況下會遵照該指令(例如,當資源被單獨的節點重用時)。
Questy 繼續向內存緩存尋求匹配的資源。不過一個也沒找到。
但 Questy 並無放棄。它經過了 Resource Timing 和 DevTools network 註冊點,在那裏註冊爲尋找資源的請求(這意味着它如今將顯示在 DevTools 以及 resource timing 中,假定它最終會找到它的資源)。
在註冊這一行政部分完成後,它繼續朝着...
與內存緩存不一樣,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 緩存,有時也被它的緩存朋友稱爲「磁盤緩存」,它與 Questy 以前看到的緩存徹底不一樣。
一方面,它是持久的,容許資源在會話之間甚至跨站點重用。若是某個資源由一個站點緩存,那麼 HTTP 緩存也容許其餘站點重用該資源。
同時,HTTP 緩存遵循 HTTP 語義(它的名字就代表了這一點)。它樂於爲其認爲「新鮮」的資源提供服務(基於緩存生命週期,由其響應的緩存頭指示),從新驗證資源,並拒絕存儲不應存儲的資源。
它是一個持久緩存,因此它也須要驅逐資源,但與 Service Worker 緩存不一樣的是,只要緩存須要空間去儲存更重要或更流行的資源時,資源就能一個接一個地被逐出。
HTTP 緩存具備一個基於內存的組件。在組件中,它會對進入的請求進行資源匹配。但當它找到了匹配的資源時,它會從磁盤中獲取資源內容,而這會是一個昂貴的操做。
咱們以前提到過,HTTP 緩存尊重HTTP語義。這個說法幾乎徹底正確,但有一個例外:HTTP 緩存會在有限的時間內存儲資源。
經過顯式的提示(`<link rel=prefetch>`)或者瀏覽器的內部策略,能爲下一個導航預獲取資源,而那些預獲取的資源會保存到下次導航,即便它們是不可緩存的。
因此當這種預獲取的資源到達 HTTP 緩存時,它會被緩存(並沒有需從新驗證)5分鐘。
複製代碼
HTTP緩存看起來至關嚴格,但 Questy 仍是鼓起勇氣問它是否有匹配的資源。答案是沒有。
它將不得不繼續走向網絡。經過網絡的旅程是可怕且不可預知的,但 Questy 明白它不管如何都必須找到它的資源。因此它繼續前進。它發現了一個 HTTP/2 會話,接着很快就會經過網絡發送,這時它忽然看到了......
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 的旅程中學到什麼?
總而言之,若是你使用 preload、H2 push、Service Worker 或其餘先進技術來嘗試加速你的網站時,你可能會注意到內部緩存實現的狀況。經過了解這些內部緩存以及它們的運行方式可能會幫助你更好地理解網站現狀,並有可能避免沒必要要問題。