瀏覽器快取機制
在談到瀏覽器快取的時候,其實可以從兩個角度出發。第一個是從快取位置出發,瀏覽器存在四種不同位置的快取;第二個是從具體的快取策略出發,也就是我們經常說的強快取和協商快取。
1)快取位置
Service Worker Cache
首先理解什麼是 web worker,web worker 是一個獨立於 JS 主執行緒之外的執行緒,可以執行一些耗時、耗資源的任務,從而分擔主執行緒的壓力。而 service worker 也屬於一種 web worker,只是它更像是一個代理伺服器,可以攔截請求和響應,實現資源的離線快取 —— 這裡的離線快取其實就是 service worker cache。
- 快取控制:由開發者透過 api 攔截請求或者響應,自己決定快取什麼資源,如何匹配資源等
- 快取時長:持久快取。即使關閉了頁面或者瀏覽器,快取也依然存在。只有兩種情況快取會消失,一是手動呼叫 api 清除快取,二是快取容量達到瀏覽器的最大限制
- 功能:用於離線儲存資源,即使無網路環境下也可以使用
Memory Cache
位於記憶體中的快取,快取的是資源的引用
- 快取控制:完全由瀏覽器內部控制的
- 快取時長:臨時快取。關閉頁面,程序就結束,記憶體就被銷燬,記憶體快取也會被清除
- 功能:一般會快取體積小的靜態資源,記憶體讀取速度比磁碟讀取速度快很多
Disk Cache(Http Cache)
位於磁碟中的快取,直接快取資源本身
- 快取控制:可以透過頭部欄位控制快取策略
- 快取時長:持久快取。即使關閉了頁面或者瀏覽器,快取也依然存在。有效期一般透過 Expire 或者 Cache-Control 的 max-age 設定
- 功能:一般會快取體積大的靜態資源,由於是儲存在磁碟上,所以讀取速度不如記憶體快取快
Push Cache
HTTP/2 引入了 server push,而 server push 使用的快取叫做 push cache。
- 快取控制:完全由瀏覽器內部控制
- 快取時長:臨時快取。會話結束快取就消失,即使會話沒結束,若過了五分鐘,快取也會消失
- 功能:也可以快取資源,但是快取只能使用一次。瀏覽器為了實現最佳化,會讓同域名的不同標籤頁共用同一個 HTTP/2 連線,而它們實際上也是會共用 push cache 的。
優先順序
首先,在快取使用的優先順序上,會按照 Service Worker Cache --> Memory Cache --> Disk Cache --> Push Cache
的順序查詢。
另一方面,使用者的互動行為也會影響具體使用哪種快取:
- 位址列輸入 url:優先查詢 Disk Cache
- F5 重新整理:優先查詢 Memory Cache,找不到再查詢 Disk Cache
- ctrl + F5 重新整理:不使用快取,重新請求
2)快取策略
具體的快取策略,分為強快取和協商快取兩種。可以先透過下面這張圖瞭解大概的過程:
優先進行強快取
- 瀏覽器針對資源 A 初次發起請求,依次檢視 ServiceWorker Cache、Memory Cache 和 Disk Cache 是否有快取。因為沒有,所以請求到達伺服器,伺服器返回資源和攜帶在頭部欄位中的快取策略
- 瀏覽器這邊自動快取資源的引用到 Memory Cache 中,同時根據頭部欄位將資源快取到 Disk Cache 中,如果開發者使用了 Service Worker Cache,也會對應做一個快取
- 片刻後,使用者再次針對資源 A 發起請求,瀏覽器會依次檢視 Memory Cache 和 Disk Cache 是否有快取,接著檢視快取是否新鮮(是否沒有過期)
- 在 http/1.0 中,服務端會返回一個 Expires 頭部欄位,它是一個絕對的到期時間,只要瀏覽器再次傳送請求的時候沒有過這個時間,就認為快取沒有過期。
- 在 http/1.1 中,Expires 被 Cache-Control 取代。而 Cache-Control 可以設定一個
max-age = <seconds>
,指的是從請求發起過了多少秒之後,快取才會過期
- 如果快取沒有過期,那麼恭喜,這時候可以直接從 Disk Cache 中獲取資源 A 並返回,我們稱這種情況為命中強快取、走強快取路線。
關於 Expires 欄位和 Cache-Control 欄位
Expires 是 HTTP/1.0 的產物,Cache-Control 是 HTTP/1.1 的產物,這兩個欄位都指定了快取的有效期。
為什麼優先使用 Cache-Control 欄位?
這是因為使用 Expires 欄位是不精確的。它來自於伺服器那邊的時間,但比較是否過期的時候,瀏覽器是拿自己的本地時間與之進行比較的,如果雙端時間不同步,那麼就會導致快取過期的判定出現問題。
而 Cache-Control 欄位設定的其實是一個相對時間,相對時間就不存在誤差了。更重要的是,它還提供了其它選項對快取策略進行控制:
-
過期時間:
規則 功能 max-age = 5 取代 expires,指定快取的有效期。自上次請求過去 5 秒後,快取才過期。對於不經常變動且想要快取的資源,可以給定一個很大的 max-age 值,再配合 url 實現更新 s-maxage = 5 可以覆蓋 max-age,但只有快取是 public 的時候才生效;若快取是 private,則無效 max-stale = 5 允許客戶端使用過期的快取,但是最多允許過期 5s,過期超過 5s 就不能用了 min-fresh = 5 客戶端要求一個新鮮的、未過期的快取,並且至少過了 5s 還是新鮮的 -
可快取性:
規則 功能 public 瀏覽器和代理伺服器都可以快取資源 private 只有瀏覽器可以快取資源 no-store 不可以快取資源,不會有強快取和協商快取之說 no-cache 可以快取資源,但是無論資源是否過期,都需要向服務端驗證資源是否更改。比較適用於那些經常更改的資源
Cache-Control 可以取代 Expires 嗎?
答案是暫時不能。在某些不支援 HTTP/1.1 的環境中,為了實現向下相容,仍然需要使用 Expires。
其次進行協商快取
如果快取已經過期,那麼瀏覽器就需要和伺服器進行協商,協商的內容就是:我應該繼續使用這個已經過期的快取資源,還是使用可能已經發生更新的新資源?這時候稱為沒有命中強快取、走協商快取路線。
​ 1)如果第一次的響應攜帶了 ETag 欄位:瀏覽器將 ETag 的欄位值作為 If-None-Match 的欄位值,向伺服器傳送條件請求,相當於是在問伺服器:==當時傳送資源給我的時候,這個資源的唯一標識是 ETag,是否這個雜湊值仍然和資源的目前最新的雜湊值一致呢?==伺服器就會拿收到的這個欄位值與目前最新的資源雜湊值進行比較,如果一致說明資源沒有發生修改,此時返回 304 狀態碼,讓瀏覽器使用之前的舊快取;如果不一致說明資源發生了修改,此時重新響應 200 和新資源給瀏覽器
​ 2)如果第一次的響應沒有攜帶 ETag 欄位,但是攜帶了 Last-Modified 欄位:瀏覽器將 Last-Modified 的欄位值作為 If-Modified-Since 的欄位值,向伺服器傳送條件請求,相當於是在問伺服器:==當時傳送資源給我的時候,最後一次修改資源的時間是 Last-Modified,是否自從這個時間之後,資源沒有再次被修改呢?==伺服器就會拿收到的這個欄位值與目前最新的資源修改時間進行比較,如果時間吻合說明資源沒有發生修改,此時返回 304 狀態碼,讓瀏覽器使用之前的舊快取;如果時間不吻合說明資源發生了修改,此時重新響應 200 和新資源給瀏覽器
​ 3)如果兩個欄位都沒有攜帶:此時就進行正常的請求響應
關於 Last-Modified 欄位和 ETag 欄位
為什麼優先使用 ETag 欄位?
使用 Last-Modified 進行校驗,實際上會有兩個問題,而這兩個問題會影響到關於“資源是否真的發生了修改”的判斷:
- 其一,把沒修改當成了修改。Last-Modified 更準確地說應該是上次編輯時間而不是上次修改時間,所謂編輯,意思就是不一定發生了修改,或者發生的是無關緊要的修改,但由於編輯時間確實改動了,所以伺服器給出的結果依然是資源發生了修改;
- 其二,把修改當成了沒修改。Last-Modified 最小隻能精確到秒這個量級,這就是說,如果資源在一秒內發生了多次修改,其實伺服器是看不出來的,給出的結果依然是資源沒有發生修改。
而如果是使用 ETag 進行校驗,因為它本質上是基於檔案內容進行編碼所產生的雜湊值,可以精確地感知檔案內容的變化,所以用來判斷資源是否發生修改,準確性會很高。
ETag 可以取代 Last-Modified 嗎?
ETag 不是銀彈,應該將其視為 Last-Modified 的補充和強化,而不是替代品。ETag 本身也有缺點,那就是每次 ETag 的生成都需要進行一次雜湊計算,對伺服器的效能有一定的影響。而相反的,Last-Modified 只需要記錄一個時間,效能要好很多。
代理伺服器快取機制
這裡只說一下響應報文中 vary 頭部欄位在代理伺服器快取中的作用。實際上,vary 欄位是協助進行內容協商的,可以防止客戶端錯誤返回快取資源。
假設有兩個客戶端 A 和 B,A 支援 gzip 編碼而 B 不支援。
- 第一次 A 發出請求,經代理伺服器轉發後到達伺服器,伺服器返回資源到代理伺服器,並且會在響應報文中增加一個 vary 欄位,比如說 vary:accept-encoding。代理伺服器會針對資源做一個快取,同時透過響應報文中的 vary 給資源打上一個標記,比如 accept-encoding:gzip,代表這個資源是使用 gzip 編碼壓縮的。
- 假設 A 在片刻後發出第二次請求,還是請求同樣的資源,因為也有一個 accept-encoding:gzip,所以代理伺服器可以返回快取給客戶端 A
- 之後假設 B 也發出了一次請求,但是由於 B 不支援 gzip 編碼,所以是不攜帶 accept-encoding:gzip 欄位的,代理伺服器不會把快取的這個資源返回給 B
因此 vary 實際上就相當於是給代理伺服器的快取資源打上一個標記,如果當時源伺服器不返回 vary 欄位,那麼 B 請求資源的時候,代理伺服器會錯誤地把資源返回給 B,而 B 是使用不了這個資源的,因為它不支援 gzip 編碼。