深入理解瀏覽器的緩存機制

一、前言

接下來的內容中我們將通過緩存位置、緩存策略以及實際場景應用緩存策略來探討瀏覽器緩存機制。

如需獲取思維導圖或想閱讀更多優質文章請猛戳 GitHub 博客

二、緩存位置

從緩存位置上來說分爲四種,並且各自有優先級,當依次查找緩存且都沒有命中的時候,纔會去請求網絡。

1.Service Worker

Service Worker 是運行在瀏覽器背後的獨立線程,一般可以用來實現緩存功能。使用 Service Worker 的話,傳輸協議必須爲 HTTPS。因爲 Service Worker 中涉及到請求攔截,所以必須使用 HTTPS 協議來保障安全。Service Worker 的緩存與瀏覽器其他內建的緩存機制不同,它可以讓我們自由控制緩存哪些文件、如何匹配緩存、如何讀取緩存,並且緩存是持續性的

Service Worker 實現緩存功能一般分爲三個步驟:首先需要先註冊 Service Worker,然後監聽到 install 事件以後就可以緩存需要的文件,那麼在下次用戶訪問的時候就可以通過攔截請求的方式查詢是否存在緩存,存在緩存的話就可以直接讀取緩存文件,否則就去請求數據。

當 Service Worker 沒有命中緩存的時候,我們需要去調用 fetch 函數獲取數據。也就是說,如果我們沒有在 Service Worker 命中緩存的話,會根據緩存查找優先級去查找數據。但是不管我們是從 Memory Cache 中還是從網絡請求中獲取的數據,瀏覽器都會顯示我們是從 Service Worker 中獲取的內容。

2.Memory Cache

Memory Cache 也就是內存中的緩存,主要包含的是當前中頁面中已經抓取到的資源, 例如頁面上已經下載的樣式、腳本、圖片等。讀取內存中的數據肯定比磁盤快, 內存緩存雖然讀取高效,可是緩存持續性很短,會隨着進程的釋放而釋放。一旦我們關閉 Tab 頁面,內存中的緩存也就被釋放了

**那麼既然內存緩存這麼高效,我們是不是能讓數據都存放在內存中呢?**這是不可能的。計算機中的內存一定比硬盤容量小得多,操作系統需要精打細算內存的使用,所以能讓我們使用的內存必然不多。

當我們訪問過頁面以後,再次刷新頁面,可以發現很多數據都來自於內存緩存

內存緩存中有一塊重要的緩存資源是 preloader 相關指令(例如 <linkrel="prefetch">)下載的資源。衆所周知 preloader 的相關指令已經是頁面優化的常見手段之一,它可以一邊解析 js/css 文件,一邊網絡請求下一個資源。

需要注意的事情是,內存緩存在緩存資源時並不關心返回資源的 HTTP 緩存頭 Cache-Control 是什麼值,同時資源的匹配也並非僅僅是對 URL 做匹配,還可能會對 Content-Type,CORS 等其他特徵做校驗

3.Disk Cache

Disk Cache 也就是存儲在硬盤中的緩存,讀取速度慢點,但是什麼都能存儲到磁盤中,比之 Memory Cache 勝在容量和存儲時效性上

在所有瀏覽器緩存中,Disk Cache 覆蓋面基本是最大的。它會根據 HTTP Herder 中的字段判斷哪些資源需要緩存,哪些資源可以不請求直接使用,哪些資源已經過期需要重新請求。並且即使在跨站點的情況下,相同地址的資源一旦被硬盤緩存下來,就不會再次去請求數據。絕大部分的緩存都來自 Disk Cache,關於 HTTP 的協議頭中的緩存字段,我們會在下文進行詳細介紹。

**瀏覽器會把哪些文件丟進內存中?哪些丟進硬盤中?**關於這點,網上說法不一,不過以下觀點比較靠得住:

4.Push Cache

Push Cache(推送緩存)是 HTTP/2 中的內容,當以上三種緩存都沒有命中時,它纔會被使用。它只在會話(Session)中存在,一旦會話結束就被釋放,並且緩存時間也很短暫,在 Chrome 瀏覽器中只有 5 分鐘左右,同時它也並非嚴格執行 HTTP 頭中的緩存指令。

Push Cache 在國內能夠查到的資料很少,也是因爲 HTTP/2 在國內不夠普及。這裏推薦閱讀 JakeArchibald的 HTTP/2 push is tougher than I thought 這篇文章,文章中的幾個結論:

如果以上四種緩存都沒有命中的話,那麼只能發起請求來獲取資源了。

那麼爲了性能上的考慮,大部分的接口都應該選擇好緩存策略,通常瀏覽器緩存策略分爲兩種:強緩存和協商緩存,並且緩存策略都是通過設置 HTTP Header 來實現的

三、緩存過程分析

瀏覽器與服務器通信的方式爲應答模式,即是:瀏覽器發起 HTTP 請求 – 服務器響應該請求,那麼瀏覽器怎麼確定一個資源該不該緩存,如何去緩存呢?瀏覽器第一次向服務器發起該請求後拿到請求結果後,將請求結果和緩存標識存入瀏覽器緩存,瀏覽器對於緩存的處理是根據第一次請求資源時返回的響應頭來確定的。具體過程如下圖:

由上圖我們可以知道:

以上兩點結論就是瀏覽器緩存機制的關鍵,它確保了每個請求的緩存存入與讀取,只要我們再理解瀏覽器緩存的使用規則,那麼所有的問題就迎刃而解了,本文也將圍繞着這點進行詳細分析。爲了方便大家理解,這裏我們根據是否需要向服務器重新發起 HTTP 請求將緩存過程分爲兩個部分,分別是強緩存和協商緩存。

四、強緩存

強緩存:不會向服務器發送請求,直接從緩存中讀取資源,在 chrome 控制檯的 Network 選項中可以看到該請求返回 200 的狀態碼,並且 Size 顯示 from disk cache 或 from memory cache。強緩存可以通過設置兩種 HTTP Header 實現:Expires 和 Cache-Control。

1.Expires

緩存過期時間,用來指定資源到期的時間,是服務器端的具體的時間點。也就是說,Expires=max-age + 請求時間,需要和 Last-modified 結合使用。Expires 是 Web 服務器響應消息頭字段,在響應 http 請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數據,而無需再次請求。

Expires 是 HTTP/1 的產物,受限於本地時間,如果修改了本地時間,可能會造成緩存失效Expires:Wed,22Oct201808:41:00GMT表示資源會在 Wed, 22 Oct 2018 08:41:00 GMT 後過期,需要再次請求。

2.Cache-Control

在 HTTP/1.1 中,Cache-Control 是最重要的規則,主要用於控制網頁緩存。比如當 Cache-Control:max-age=300時,則代表在這個請求正確返回時間(瀏覽器也會記錄下來)的 5 分鐘內再次加載資源,就會命中強緩存。

Cache-Control 可以在請求頭或者響應頭中設置,並且可以組合使用多種指令:

public所有內容都將被緩存(客戶端和代理服務器都可緩存)。具體來說響應可被任何中間節點緩存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中間的 proxy 可以緩存資源,比如下次再請求同一資源 proxy1 直接把自己緩存的東西給 Browser 而不再向 proxy2 要。

private所有內容只有客戶端可以緩存,Cache-Control 的默認取值。具體來說,表示中間節點不允許緩存,對於 Browser <-- proxy1 <-- proxy2 <-- Server,proxy 會老老實實把 Server 返回的數據發送給 proxy1, 自己不緩存任何數據。當下次 Browser 再次請求時 proxy 會做好請求轉發而不是自作主張給自己緩存的數據。

no-cache:客戶端緩存內容,是否使用緩存則需要經過協商緩存來驗證決定。表示不使用 Cache-Control 的緩存控制方式做前置驗證,而是使用 Etag 或者 Last-Modified 字段來控制緩存。需要注意的是,no-cache 這個名字有一點誤導。設置了 no-cache 之後,並不是說瀏覽器就不再緩存數據,只是瀏覽器在使用緩存數據時,需要先確認一下數據是否還跟服務器保持一致。

no-store:所有內容都不會被緩存,即不使用強制緩存,也不使用協商緩存

max-age:max-age=xxx (xxx is numeric) 表示緩存內容將在 xxx 秒後失效

s-maxage(單位爲 s):同 max-age 作用一樣,只在代理服務器中生效(比如 CDN 緩存)。比如當 s-maxage=60 時,在這 60 秒中,即使更新了 CDN 的內容,瀏覽器也不會進行請求。max-age 用於普通緩存,而 s-maxage 用於代理緩存。s-maxage 的優先級高於 max-age。如果存在 s-maxage,則會覆蓋掉 max-age 和 Expires header。

max-stale:能容忍的最大過期時間。max-stale 指令標示了客戶端願意接收一個已經過期了的響應。如果指定了 max-stale 的值,則最大容忍時間爲對應的秒數。如果沒有指定,那麼說明瀏覽器願意接收任何 age 的響應(age 表示響應由源站生成或確認的時間與當前時間的差值)。

從圖中我們可以看到,我們可以將多個指令配合起來一起使用,達到多個目的。比如說我們希望資源能被緩存下來,並且是客戶端和代理服務器都能緩存,還能設置緩存失效時間等等。

3.Expires 和 Cache-Control 兩者對比

五、協商緩存

協商緩存就是強制緩存失效後,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程,主要有以下兩種情況

協商緩存可以通過設置兩種 HTTP Header 實現:Last-Modified 和 ETag 。 

1.Last-Modified 和 If-Modified-Since

瀏覽器在第一次訪問資源時,服務器返回資源的同時,在 response header 中添加 Last-Modified 的 header,值是這個資源在服務器上的最後修改時間,瀏覽器接收後緩存文件和 header;

Last
-
Modified
:
 
Fri
,
 
22
 
Jul
 
2016
 
01
:
47
:
00
 GMT

瀏覽器下一次請求這個資源,瀏覽器檢測到有 Last-Modified 這個 header,於是添加 If-Modified-Since 這個 header,值就是 Last-Modified 中的值;服務器再次收到這個資源請求,會根據 If-Modified-Since 中的值與服務器中這個資源的最後修改時間對比,如果沒有變化,返回 304 和空的響應體,直接從緩存讀取,如果 If-Modified-Since 的時間小於服務器中這個資源的最後修改時間,說明文件有更新,於是返回新的資源文件和 200

但是 Last-Modified 存在一些弊端:

既然根據文件修改時間來決定是否緩存尚有不足,能否可以直接根據文件內容是否修改來決定緩存策略?所以在 HTTP / 1.1 出現了 ETagIf-None-Match

2.ETag 和 If-None-Match

Etag 是服務器響應請求時,返回當前資源文件的一個唯一標識 (由服務器生成),只要資源有變化,Etag 就會重新生成。瀏覽器在下一次加載資源向服務器發送請求時,會將上一次返回的 Etag 值放到 request header 裏的 If-None-Match 裏,服務器只需要比較客戶端傳來的 If-None-Match 跟自己服務器上該資源的 ETag 是否一致,就能很好地判斷資源相對客戶端而言是否被修改過了。如果服務器發現 ETag 匹配不上,那麼直接以常規 GET 200 回包形式將新的資源(當然也包括了新的 ETag)發給客戶端;如果 ETag 是一致的,則直接返回 304 知會客戶端直接使用本地緩存即可。

3. 兩者之間對比:

Last-Modified 的時間單位是秒,如果某個文件在 1 秒內改變了多次,那麼他們的 Last-Modified 其實並沒有體現出來修改,但是 Etag 每次都會改變確保了精度;如果是負載均衡的服務器,各個服務器生成的 Last-Modified 也有可能不一致。

六、緩存機制

強制緩存優先於協商緩存進行,若強制緩存 (Expires 和 Cache-Control) 生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match),協商緩存由服務器決定是否使用緩存,若協商緩存失效,那麼代表該請求的緩存失效,返回 200,重新返回資源和緩存標識,再存入瀏覽器緩存中;生效則返回 304,繼續使用緩存。具體流程圖如下:

看到這裏,不知道你是否存在這樣一個疑問: 如果什麼緩存策略都沒設置,那麼瀏覽器會怎麼處理?

對於這種情況,瀏覽器會採用一個啓發式的算法,通常會取響應頭中的 Date 減去 Last-Modified 值的 10% 作爲緩存時間。

七、實際場景應用緩存策略

1. 頻繁變動的資源

Cache-Control: no-cache

對於頻繁變動的資源,首先需要使用 Cache-Control:no-cache 使瀏覽器每次都請求服務器,然後配合 ETag 或者 Last-Modified 來驗證資源是否有效。這樣的做法雖然不能節省請求數量,但是能顯著減少響應數據大小。

2. 不常變化的資源

Cache-Control: max-age=31536000

通常在處理這類資源時,給它們的 Cache-Control 配置一個很大的 max-age=31536000 (一年),這樣瀏覽器之後請求相同的 URL 會命中強制緩存。而爲了解決更新的問題,就需要在文件名 (或者路徑) 中添加 hash, 版本號等動態字符,之後更改動態字符,從而達到更改引用 URL 的目的,讓之前的強制緩存失效 (其實並未立即失效,只是不再使用了而已)。在線提供的類庫 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均採用這個模式。

八、用戶行爲對瀏覽器緩存的影響

所謂用戶行爲對瀏覽器緩存的影響,指的就是用戶在瀏覽器如何操作時,會觸發怎樣的緩存策略。主要有 3 種:

參考文章

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/A93VNkEKTVb982knhVN4eQ