關於 Web 快取的那些風流事兒

李猜猜發表於2017-04-11

最近大家針對preload、HTTP/2 push和ServiceWorker的瀏覽器快取實現展開了激烈的討論,而這也引起了很多人的疑惑。

鑑於此,我想講個故事來讓大家瞭解一個請求如何完成他的使命並找到匹配的快取資源,

以下內容均基於 Chromium 的術語,不過其餘瀏覽器的實現本質上沒有太大的差異。

Questy 的旅程

Questy 是一個請求。她是在渲染引擎內(也叫渲染器)誕生的。她渴望能在這個標籤頁關閉前找到一個讓她的“人生”再無遺憾的資源。

所以 Questy 展開了她追求幸福的旅程。 但是她會在哪裡找到一個恰恰適合的資源呢?

此時離她最近的是……

記憶體快取(Memory Cache)

記憶體快取中包含了大量的資源。他包含了所有渲染引擎請求的資源。這些資源都是現有文件的一部分。在文件的生命週期中他們都會被儲存在此。這意味著,如果 Questy 尋找的資源已經被文件中的其餘部分載入了,那麼他們會在此相遇。

確切來說,“短期記憶體快取”這個名字可能會更適合。因為內容快取僅在導航結束前儲存這些資源,在某些情況下,時間甚至會更短。

事實上,很多種情況都會導致 Questy 尋找的資源已經被載入。

預載入器(preloader)可能是最常發生的情況。如果 Questy 是由 HTML 解析器創造的 DOM節點所激發的,那麼她很可能會發現,她所尋找的資源早已在 HTML 標記化階段載入完畢了。

顯示 preload 指令(<link rel=preload>)則是另一種較為可能發生的情況。該指令會讓瀏覽器預載入資源並儲存在記憶體快取中。

除此之外,還有可能是因為所請求的資源與之前的 DOM 節點或者 CSS 規則所需要的資源相同。例如,一個頁面中可能會含有多個具有相同 src 屬性的 <img> 元素,但是他們會得到同一個資源。而實現這種機制的正是記憶體快取。

然而,記憶體快取不會輕易匹配我們的資源請求。當然了,為了使請求和資源相匹配,他們必須要有相同的 URL 。不過,這還不是全部。他們還必須要有相同的資源型別(這樣子一個指令碼資源才不會被一個圖片請求所匹配),相同的 CORS 策略和一些其他特性。

規範並沒有十分地明確定義記憶體快取所需要匹配的特性,所以不同的瀏覽器的實現可能會有一定的差異。

有一樣東西是記憶體快取不關心的,那就是 HTTP 語義。無論資源的頭部是是否帶有 max-age=0 或者 no-cache 、Cache-Control標籤,記憶體快取都不關心。因為在當前導航中,資源是可以重用的,所以 HTTP 語義並不重要。

唯一例外的是no-store指令。在某些特定的情況下瀏覽器會尊重他。(例如,當資源被單獨節點重用時)。

所以,Questy 走上前詢問記憶體快取是否有匹配的資源。唉,然而並沒有。

Questy 並沒有放棄。她走過資源計時器和開發者工具的網路註冊點。在那裡,她註冊為尋找資源的請求(這意味著如果她能找到匹配的資源,則會出現在開發中工具和資源計時器中)。

完成了這些官方登記後,她繼續向前……

Service Worker 快取

和記憶體快取不一樣,Service Woker喜歡不走尋常路。他的行為難以預測。因為他只遵循開發者告訴他的規則。

首先,Service Worker只有安裝後才會存在。而且因為他的邏輯是由開發者編寫的 JavaScript 而不是瀏覽器控制的,所以 Questy 完全不知道她能不能在這裡找到那個他?那個資源長成什麼的?他是被儲存在快取裡嗎?還是說他是由 Service Worker 的主人精心偽造的響應?

這些問題沒有人可以回答她。因為 Service Worker 自成一套,無論是資源的匹配方式還是響應的包裝方法,他們都能按照自己的的想法去完成。

Service Worker 擁有和快取相關的 API ,這讓他可以儲存資源。和記憶體儲存不同的是這種儲存方式是持久的。即使該標籤頁被關閉甚至瀏覽器重啟,這些被儲存的資源都不會丟失。只有當開發者明確表示要移除他們的時候(使用 cache.delete(resource)),他們才會被移除。另外一種情況就是當瀏覽器的儲存空間不足時,他會將整個 Service Worker 快取還有其他源儲存如 indexedDB、localStorage 等都清除掉。也因此,Service Worker 能確保他的儲存和其他源儲存是同步的。

Service Worker 只負責特定的域,換言之,他最多隻能管理一個 host。因此,Service Worker 只能控制來自特定域內的文件的請求。

Questy 走向 Service Worker 詢問他有沒有合適的資源。可惜的是 Service Worker 從來沒有見過那個域的資源,所以他也找不到 Questy 尋找的請求了。於是,Service Worker 讓 Questy 繼續前行(通過 fetch()),從而在網路棧這片神奇的土地裡繼續尋找她需要的資源。

而一旦進入網路棧,最容易找到資源的地方就是……

HTTP 快取

HTTP 快取(有時候也被他的朋友成為“磁碟快取”)和 Questy 之前遇到過的快取不太一樣。

一方面,他們的儲存是持久的,而且能被不同的會話甚至不同的網站重用。如果一個資源被一個網站下載了,他也可以被其他網站重用,

而另一方面,HTTP 快取遵循 HTTP 語義(名字早已暗示了一切)。他樂於提供他認為覺得是“新鮮”的資源(基於由響應的快取頭宣告的生命週期)、校驗那些需要重新驗證的資源、並拒絕儲存那些它不應該儲存的資源。

既然他是一個永續性的快取,他也需要移除資源。但和 Service Worker 不一樣的事,他會在覺得他需要空間來儲存更重要或者會被更多人需要的資源時,逐個移除那些舊資源。

HTTP 快取擁有一個基於記憶體的元件。他負責為請求匹配資源。可是一旦資源匹配成功,它需要從磁碟中獲取資源內容,這是一個較為昂貴的操作。

上文我們提到 HTTP 快取遵循 HTTP 語義。這基本是正確的。除了一個例外情況,HTTP 快取會儲存一些資源一段時間。瀏覽器能夠為下次導航預取資源。我們可以通過顯示的指令(<link rel=prefetch>)或者依靠瀏覽器內部機制完成。這些被預取的資源會被儲存下來直到下次導航,儘管它們可能是不允許快取的。所以當預取資源到達 HTTP 快取時,它會被快取(並且不需要校驗就會被提供)大概五分鐘。

儘管 HTTP 快取看起來十分的嚴厲,但 Questy 還是鼓起勇氣上前詢問有沒有匹配的資源。然而答案依舊是沒有。

她還是得繼續隨著網路往前走。這段旅程時可怕而且未知的,然而 Questy 知道無論如何她都要找到她需要的資源。所以她只能繼續。這時候她找到了一個對應的 HTTP/2 會話。並且準備通過網路繼續前行,這時候她忽然看到了……

推送“快取”

推送快取(其實他更應該被描述為“待認領的推送流儲存器”,不過那實在是太拗口了)是儲存 HTTP/2 推送資源的地方。它們是 HTTP/2 會話的一部分,這有幾個特殊的含義。

這個容器並不是持久的。當會話結束後,未被認領的資源(例如,從來沒有被請求匹配到的)就會被移除。如果資源是由不同的 HTTP/2 會話獲取的,他們並不會匹配。除此之外,推送快取只會儲存資源一段時間(在基於 Chromium 的瀏覽器裡,這個時長約為五分鐘)。

推送快取根據請求的 URL 和請求頭匹配相應,但他不遵循嚴格的 HTTP 語義。

規範裡也沒有明確定義推送快取,所以再各個瀏覽器、系統或者 HTTP/2 客戶端間的實現可能會不一樣。

儘管信心不大,Questy 還是上前詢問是否有匹配的請求。令人驚訝的是,他真的有!!Questy 喜出望外的認領了這個資源(這也意味著它將這個 HTTP/2 流從待認領容器中移除)。現在她可以回去渲染這個資源了。

在他們回程的路上,他們走過了 HTTP 快取,並且話費了一些時間去複製了一份資源以備日後使用。

離開網路棧後,他們回到 Service Worker 的轄區,而 Service Worker 也將一份資源的拷貝儲存到自己的快取中才讓他們回到渲染器裡。

最終,一旦它們會到渲染器,記憶體快取就會儲存一份資源的引用(而不是拷貝)。這樣子在稍後如果在同一個導航會話中需要這份資源,他就可以將相同的資源分配給他。

於是,它們就幸福快活的住在了一起,直到文件被移除,然後他們都被垃圾回收了。

不過那是另外一天的故事了。

要點

所以,從 Questy 旅程中我們能學習到什麼呢?

  • 不同的請求可以從不同的瀏覽器快取中匹配的資源。
  • 請求匹配資源的快取的不同會影響這個請求是否會被開發者工具和資源計時器所展示。
  • 推送資源不會被持續儲存除非他們被請求所認領。
  • 不能儲存的預載入資源在下一個導航時不會存在。這是預載入(preload)和預取(prefetch)間的最大區別。
  • 因為還有很多地方規範沒有明確定義,所以不同的瀏覽器實現會有差異。我們需要彌補這些差異。

總而言之,如果你使用預載入,HTTP/2 推送, Service Worker 又或者其他高階技術來加速你的網站,你可能會注意到內部快取的實現情況。瞭解這些內部快取和他們的運作方式能讓你更好的解決問題並且減少不必要的麻煩。

相關文章