深入剖析瀏覽器快取策略

橘子小睿發表於2019-04-03

前言

在訪問一個網頁時,客戶端會從伺服器下載所需的資源。但是有些資源很少發生變動,例如 HTML、JS、CSS、圖片、字型檔案等。如果每次載入頁面都從源伺服器下載這些資源,不僅會增加獲取資源的時間,也會給伺服器帶來一定壓力。因此,重用已獲取的資源十分重要。將請求的資源快取下來,下次請求同一資源時,直接使用儲存的副本,而不會再去源伺服器下載。這就是我們常說的快取技術。

快取的種類很多:瀏覽器快取、閘道器快取、CDN 快取、代理伺服器快取等。這些快取大致可以歸為兩類:共享快取和私有快取。共享快取能夠被多個使用者使用,而私有快取只能用於單個使用者。瀏覽器快取只存在於每個單獨的客戶端,因此它是私有快取。

本文主要介紹私有(瀏覽器)快取。你將學習:

  • 瀏覽器快取的分類
  • 如何啟用和禁止快取
  • 快取儲存的位置
  • 如何設定快取的過期時間
  • 快取過期之後會發生什麼
  • 如何為自己的應用制定合適的快取策略
  • 除錯
    • 如何判斷網站是否啟用了快取
    • 如何禁用瀏覽器快取

快取儲存

啟用快取

Cache-Control

瀏覽器會根據 HTTP Response Headers 中的一些欄位來決定是否要快取該資源。通過設定 Response Headers 中的 Cache-Control 和 Expires 可以啟用快取,這樣資源就會被快取到客戶端。

Cache-Control 可以設定 private 、publicmax-age 、 no-cache 來啟用快取。

Cache-Control: private/public
Cache-Control: max-age=300
Cache-Control: no-cache
複製程式碼
  • private :表示該資源只能被瀏覽器快取。
  • public :表示該資源既能被瀏覽器快取,也能被任何中間人(比如代理伺服器、CDN 等)快取。
  • max-age :表示該資源能夠被快取的最大時間。如果設定 max-age=0 ,該資源仍然會被瀏覽器快取,只不過立刻就過期了。
  • no-cache :該資源會被快取,但是立刻就過期了,因此需要先和伺服器確認資源是否發生變化,只有當資源沒有變化時,該快取才會被使用,否則需要從伺服器下載。相當於 max-age=0 。

Expires

Expires 標識了快取的具體過期時間,來控制資源何時過期。通過設定 Expires 可以啟用快取。不過需要注意 Expires 的值是格林威治時間(Greenwich Mean Time, GMT),不是本地時間。

Expires: Fri, 08 Mar 2029 08:05:59 GMT
Expires: 0 // Expires: 0 仍然會啟用快取,只不過快取立刻過期。
複製程式碼

優先順序

既然 Cache-Control 和 Expires 都能夠啟用快取,那麼問題來了,如果同時設定 Cache-Control: max-age=600 和 Expires: 0 ,那麼瀏覽器應該如何快取該資源呢?答案是隻有 Cache-Control: max-age=600 生效。因為 Cache-Control 的優先順序高於 Expires,如果同時設定了 Cache-Control 和 Expires,以 Cache-Control 為準。

瀏覽器的預設行為

設定 Cache-Control 之後,可以看到瀏覽器確實啟用了快取(from disk cache)。如下所示:

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

Screen Shot 2019-04-01 at 5.26.58 PM.png

但是我發現,即使 Response Header 中沒有設定 Cache-Control 和 Expires,瀏覽器仍然會快取某些資源。這是為什麼呢?

image.png

原來當 Response Header 中有 Last-Modified 但是沒有 Cache-Control 和 Expires 時,瀏覽器會用一套自己的演算法來決定這個資源會被快取多長時間。這是瀏覽器為了提升效能進行的優化,每個瀏覽器的行為可能不一致,有些瀏覽器上甚至沒有這樣的優化。因此,如果要啟用快取,還是應該自己設定合適的 Cache-Control 和 Expires,不要依賴瀏覽器自身的快取演算法。當然,如果在除錯時發現本應該更新的檔案沒有更新,也別忘了看看是否被瀏覽器快取了。

禁止快取

給 Cache-Control 設定 no-store 會禁止瀏覽器和中間人快取該資源。在處理包含個人隱私資料或銀行業務資料的資源時很有用。

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

快取目標物件

一般來說,瀏覽器快取只能儲存 GET 響應,例如 HTML、JS、CSS、圖片等靜態資源。因為這些資源不經常發生變化,所以快取可以幫助提升獲取資源的速度。但是像一些 POST/DELETE 請求,這些請求基本上每一次都不一樣,因此也沒有什麼快取的價值。

快取位置

瀏覽器可以在記憶體、硬碟中開闢一個空間用以儲存請求資源的副本。我們經常在 Dev Tools 裡面看到 Memory Cache(記憶體快取)和 Disk Cache(硬碟快取),指的就是快取所在的位置。請求一個資源時,會按照優先順序(Service Worker -> Memory Cache -> Disk Cache -> Push Cache)依次查詢快取,如果命中則使用快取,否則發起網路請求。這裡只介紹常用的 Memory Cache 和 Disk Cache。

Screen Shot 2019-04-02 at 2.24.56 PM.png

200 from Memory Cache

表示不訪問伺服器,直接從記憶體中讀取快取。因為快取的資源儲存在記憶體中,所以讀取速度較快,但是關閉程式之後,快取的資源也會隨之銷燬。一般來說,系統不會給記憶體分配較大的容量,因此記憶體快取一般用於儲存小檔案。同時,記憶體快取在有時效性要求的場景下也很有用(比如瀏覽器的隱私模式)。

200 from Disk Cache

表示不訪問伺服器,直接從硬碟中讀取快取。與記憶體相比,硬碟的讀取速度較慢,但是硬碟快取持續的時間更長,關閉程式之後,快取的資源仍然存在。由於硬碟的容量較大,因此一般用於儲存大檔案。

總的來說就是:

記憶體快取:讀取快、持續時間短、容量小

硬碟快取:讀取慢、持續時間長、容量大

快取分類

瀏覽器快取一般分為兩類:強快取(也稱本地快取)和協商快取(也稱弱快取)。判定過程如下:

  1. 瀏覽器傳送請求前,會先去快取裡面檢視是否命中強快取,如果命中,則直接從快取中讀取資源,不會傳送請求到伺服器。否則,進入下一步。
  2. 當強快取沒有命中時,瀏覽器一定會向伺服器發起請求。伺服器會根據 Request Header 中的一些欄位來判斷是否命中協商快取。如果命中,伺服器會返回響應,但是不會攜帶任何響應實體,只是告訴瀏覽器可以直接從快取中獲取這個資源。否則,進入下一步。
  3. 如果前兩步都沒有命中,則直接從伺服器載入資源。

強快取和協商快取的共同點在於,如果命中,都是從客戶端快取中載入資源,而不是從伺服器載入資源。而不同點在於,強快取不傳送請求到伺服器,而協商快取會傳送請求到伺服器以驗證資源是否過期。普通重新整理會啟用協商快取,忽略強快取。只有在位址列或收藏夾輸入網址、通過連結引用資源等情況下,瀏覽器才會啟用強快取。

快取過期策略

當快取過期之後,瀏覽器會向伺服器發起 HTTP 請求,以確定資源是否發生了變化。如果資源未改變,那麼瀏覽器會繼續使用本地的快取資源;如果該資源已經發生變化了,那麼瀏覽器會刪除舊的快取資源,並將新的資源快取到本地。

過期時間

Http Response Header 裡面的 Cache-Control: max-age=xxx 和 Expires 都可以設定快取的過期時間,但是它們有一些區別:

Expires :標識該資源過期的時間點,它是一個絕對值,即在這個時間點之後,快取的資源過期。
max-age :標識該資源能夠被快取的最大的時間。它是一個相對值,相對於第一次請求該文件時伺服器記錄的「請求發起時間」。

雖然 Cache-Control 是 HTTP 1.1 提出來的新特性,但並不是說 max-age 優於 Expires。它們都有各自的使用場景,我們應該根據業務需求去決定使用哪一個。比如當某個資源需要在特定的時間點過期時應該使用 Expires 。如果只是為了開啟快取,使用 max-age 可能會更好些,因為 Cache-Control 的優先順序高於 Expires。

針對應用中幾乎不會改變的檔案,通常可以設定一個較長的過期時間,以保證快取的有效。例如圖片、CSS、JS 等靜態資源。

快取驗證

上一小節已經提到,當瀏覽器請求一個資源時,如果發現快取中有該資源,但是已經過期了,那麼瀏覽器就會向伺服器發起 HTTP 請求,以驗證快取的資源是否發生變化。

快取驗證時機

什麼時候會進行快取驗證?

  1. 重新整理頁面。一般來說,為了確保使用者獲取到最新的資料,在重新整理頁面時大部分瀏覽器都不會再使用快取中的資料,而是發起一個請求去伺服器驗證。
  2. Response Header 中設定了 Cache-control: must-revalidate。當快取的資源過期之後,必須到源伺服器去驗證,只有確認該資源沒有過期,才能繼續使用快取。

快取驗證器

伺服器是怎麼判斷資源改變與否的呢?服務端在返回響應內容的同時,還會在 Response Header 中設定一些驗證標識,當快取的資源過期之後,瀏覽器就會攜帶驗證標識向伺服器發起請求,伺服器通過對比這些標識,就能知道快取的資源是否發生了改變。

image.png

Header 中的驗證標識欄位主要有兩組:Etag 和 If-None-Match 、Last-Modified 和 If-Modified-Since 。其中,形如 If-xxx 這樣的請求首部欄位,可以稱之為條件請求。比如只在滿足某個條件的情況下返回或上傳檔案,這樣可以節省頻寬。


深入剖析瀏覽器快取策略

Last-Modified

Last-Modified 就是一個驗證器。伺服器在將資源返回給客戶端的同時,會將資源的最後修改時間 Last-Modified 加在 Response Header 中一起返回。瀏覽器會為資源標記上該資訊,當快取過期之後,瀏覽器會把該資訊設定到 Request Header 中的 If-Modified-Since 中向伺服器發起請求。

如果 If-Modified-Since 中的值和伺服器上該資源最終的修改時間一致,就說明該資源沒有被修改過,伺服器會直接返回 304 狀態碼,無響應實體,這樣就可以節省傳輸的資料量。如果不一致,伺服器會返回 200 狀態碼,同時和第一次 HTTP 請求一樣,返回響應實體和驗證器。

Last-Modified:Fri, 04 Jan 2019 14:00:21 GMT
複製程式碼

Etag

伺服器會通過某種演算法,為資源計算出一個唯一識別符號,在把響應返回給客戶端的時候,會在 Response Header 中加上 Etag: 唯一識別符號 一起返回給客戶端。

Etag:"952d03d8561454120b550f0a5679a172c4822ce8"
複製程式碼

客戶端會將 Etag 儲存下來,後續請求時會將 Etag 作為 Request Header 中 If-None-Match 的值發給伺服器。通過比對客戶端發過來的 Etag 和伺服器上儲存的 Etag 是否一致,就能夠知道資源是否發生了變化。如果資源沒有發生變化,返回 304,客戶端繼續使用快取。如果資源已經修改,則返回 200。

制定快取策略

快取真的可以說讓我們又愛又恨。在開發時,我們經常遇到這樣的問題:明明已經修改了這個檔案,為什麼沒有生效?好吧,檔案被快取了。。。但是在上線時,我們又希望檔案儘可能地被瀏覽器快取,來提高效能。因此為自己的應用制定合適的快取策略非常重要。

為靜態資源設定較長快取時間。

有些資源很長時間都不會改變,比如一些三方庫,圖片,字型檔案等。可以為它們設定一個很長的過期時間,例如設定「一年」。

通過給檔名的唯一標識來確保檔案修改生效。

有些時候為了解決 bug,我們可能會修改一些檔案,比如應用的 CSS、JS 等。如果這些檔案已經被快取,那麼除非使用者強制重新整理頁面,否則使用者只有在快取過期之後才有可能獲取新的檔案。如何讓瀏覽器不使用快取,而是重新下載新的檔案呢?有一個辦法就是給檔名加上唯一標識,比如 Hash 或版本資訊。當檔案修改之後,這個唯一標識也會隨之改變。瀏覽器發現檔案改變之後,就不會使用快取了。

明確是否有資源不能被快取。

比如一些敏感資料,如果不應該被瀏覽器快取,需要在 Response Header 中設定 Cache-Control: no-store。

除錯

如何知道請求的資源是否被快取了?開啟 Chrome 的開發者工具,我們可以看到 Size 這一欄下面,如果顯示檔案真實的大小,則說明該檔案未被快取。如果顯示 from xxx cache,則說明該請求使用的是已被快取的檔案。如下:

Screen Shot 2019-04-03 at 11.16.58 AM.png

除錯時,如果想禁用瀏覽器快取,可以在開發者工具上勾選 Disabel cache。

Screen Shot 2019-04-03 at 11.36.57 AM.png

最後

最後,大家可以通過下面這張圖再回顧一下我們剛剛講過的內容。

深入剖析瀏覽器快取策略

參考

相關文章