詳解瀏覽器緩存
1、緩存的作用
對于一個數據請求來說,可以分為發起網絡請求、后端處理、瀏覽器響應三個步驟。瀏覽器緩存可以幫助我們在第一和第三步驟中優化性能。比如說直接使用緩存而不發起請求,或者發起了請求但后端存儲的數據和前端一致,那么就沒有必要再將數據回傳回來,這樣就減少了響應數據。
2、瀏覽器緩存的位置
從緩存位置上來說分為四種,并且各自有優先級,當依次查找緩存且都沒有命中的時候,才會去請求網絡。
- Service Worker
- Memory Cache(內存緩存)
- Disk Cache(硬盤緩存)
- Push Cache
資源被緩存到內存還是磁盤中, 由瀏覽器自己決定,決定因素是資源的種類和大小,比如幾b大小的文件就會緩存到內存)。如果以上四種緩存都沒有命中的話,那么瀏覽器只能發起請求來獲取資源了。
2.1、Service Worker
Service Worker:Service Worker 是運行在瀏覽器背后的獨立線程,一般可以用來實現緩存功能。使用 Service Worker的話,傳輸協議必須為 HTTPS。
Service Worker 的緩存與瀏覽器其他內建的緩存機制不同,它可以讓我們自由控制緩存哪些文件、如何匹配緩存、如何讀取緩存,并且緩存是持續性的。
2.2、Memory Cache(內存緩存)
2.3、Disk Cache(硬盤緩存)
Disk Cache 也就是存儲在硬盤中的緩存,讀取速度慢點,但是什么都能存儲到磁盤中,比之 Memory Cache 勝在容量和存儲時效性上。
在所有瀏覽器緩存中,Disk Cache 覆蓋面基本是最大的。它會根據 HTTP Herder 中的字段判斷哪些資源需要緩存,哪些資源可以不請求直接使用,哪些資源已經過期需要重新請求。并且即使在跨站點的情況下,相同地址的資源一旦被硬盤緩存下來,就不會再次去請求數據。絕大部分的緩存都來自 Disk Cache。
瀏覽器會把哪些文件丟進內存中?哪些丟進硬盤中? 關于這點,網上說法不一,不過以下觀點比較靠得住:
- 對于大文件來說,大概率是不存儲在內存中的,反之優先
- 當前系統內存使用率高的話,文件優先存儲進硬盤
2.4、Push Cache
3、緩存策略
那么為了性能上的考慮,大部分的接口都應該選擇好緩存策略,通常瀏覽器緩存策略分為兩種:強緩存和協商緩存,并且緩存策略都是通過設置 HTTP Header 來實現的。
強制緩存優先于協商緩存進行。若強制緩存(Expires和Cache-Control)生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified / If-Modified-Since和Etag / If-None-Match)。協商緩存由服務器決定是否使用緩存,若協商緩存失效,那么代表該請求的緩存失效,請求響應碼返回200,重新返回資源和緩存標識,再存入瀏覽器緩存中;生效則返回304,繼續使用緩存。
優先級:
Cache-Control > expires > Etag > Last-Modified
| 緩存類型 | 獲取資源形式 | 狀態碼 | 是否發送請求到服務器 |
| 強制緩存 | 從緩存取 | 200(from cache) | 否,直接從緩存取 |
| 協商緩存 | 從緩存取 | 304(Not Modified) | 是,通過服務器來告知緩存是否可用 |
3.1、瀏覽器緩存策略

由上圖我們可以知道:
-
瀏覽器每次發起請求,都會先在瀏覽器緩存中查找該請求的結果以及緩存標識
-
瀏覽器每次拿到返回的請求結果都會將該結果和緩存標識存入瀏覽器緩存中
以上兩點結論就是瀏覽器緩存機制的關鍵,它確保了每個請求的緩存存入與讀取。
3.1.1、緩存機制流程圖
強制緩存優先于協商緩存進行,若強制緩存(Expires和Cache-Control)生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified / If-Modified-Since和Etag / If-None-Match),協商緩存由服務器決定是否使用緩存,若協商緩存失效,那么代表該請求的緩存失效,返回200,重新返回資源和緩存標識,再存入瀏覽器緩存中;生效則返回304,繼續使用緩存。具體流程圖如下:
如果什么緩存策略都沒設置,那么瀏覽器會怎么處理?對于這種情況,瀏覽器會采用一個啟發式的算法,通常會取響應頭中的 Date 減去 Last-Modified 值的 10% 作為緩存時間。
4、強緩存(不會發起請求,直接命中緩存)
強緩存:不會向服務器發送請求,直接從緩存中讀取資源。
在 Chrome 中強緩存的請求狀態碼返回是 200,并且 Size 顯示 from disk cache 或 from memory cache 。而在 Fire Fox中,from cache 狀態碼是 304,不同瀏覽器的策略可能有些許不同。
Chrome 瀏覽器強制緩存示例如下:

強緩存可以通過設置兩種 HTTP Header 實現:Expires 和 Cache-Control。兩者同時存在的話,Cache-Control優先級高于Expires。強緩存判斷是否緩存的依據來自于是否超出某個時間或者某個時間段,而不關心服務器端文件是否已經更新,所以就可能會導致加載文件不是服務器端最新的內容。
4.1、Expires
緩存過期時間,用來指定資源到期的時間,是服務器端的具體的時間點。也就是說,Expires=max-age + 請求時間,需要和Last-modified結合使用。Expires是Web服務器響應消息頭字段,在響應http請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數據,而無需再次請求。
Expires 是 HTTP/1 的產物,受限于本地時間,如果修改了本地時間,可能會造成緩存失效。比如:Expires: Wed, 22 Oct 2018 08:41:00 GMT表示資源會在 Wed, 22 Oct 2018 08:41:00 GMT 后過期,需要再次請求。
Expires 是 http 1.0 的規范,值是一個GMT 格式的時間點字符串,比如 Expires:Mon,18 Oct 2066 23:59:59 GMT 。這個時間點代表資源失效的時間,如果當前的時間戳在這個時間之前,則判定命中緩存。有一個缺點是,失效時間是一個絕對時間,如果服務器時間與客戶端時間偏差較大時,就會導致緩存混亂。而服務器的時間跟用戶的實際時間是不一樣是很正常的,所以 Expires 在實際使用中會帶來一些麻煩。
4.2、Cache-Control
在HTTP/1.1中,Cache-Control是最重要的規則,主要用于控制網頁緩存。比如當Cache-Control:max-age=300時,則代表在這個請求正確返回時間(瀏覽器也會記錄下來)的5分鐘內再次加載資源,就會命中強緩存。如果 Cache-Control與 Expires 同時存在的話, Cache-Control 的優先級高于 Expires 。
Cache-Control這個字段是 http 1.1 的規范,一般常用該字段的 max-age 值來進行判斷,它是一個相對時間,比如 .Cache-Control:max-age=3600 代表資源的有效期是 3600 秒。并且返回頭中的 Date 表示消息發送的時間,表示當前資源在 Date ~ Date +3600s 這段時間里都是有效的。不過我在實際使用中常常遇到設置了 max-age 之后,在 max-age 時間內重新訪問資源卻會返回 304 not modified ,這是由于服務器的時間與本地的時間不同造成的。
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表示響應由源站生成或確認的時間與當前時間的差值)。
- min-fresh:能夠容忍的最小新鮮度。min-fresh標示了客戶端不愿意接受新鮮度不多于當前的age加上min-fresh設定的時間之和的響應。

從圖中我們可以看到,我們可以將多個指令配合起來一起使用,達到多個目的。比如說我們希望資源能被緩存下來,并且是客戶端和代理服務器都能緩存,還能設置緩存失效時間等等。
4.2.1、Expires和Cache-Control的區別
其實這兩者差別不大,區別就在于 Expires 是http1.0的產物,Cache-Control是http1.1的產物,兩者同時存在的話,Cache-Control優先級高于Expires;在某些不支持HTTP1.1的環境下,Expires就會發揮用處。所以Expires其實是過時的產物,現階段它的存在只是一種兼容性的寫法。
5、協商緩存(會重新發起請求,由服務器決定是否使用緩存)
強緩存判斷是否緩存的依據來自于是否超出某個時間或者某個時間段,而不關心服務器端文件是否已經更新,這可能會導致加載文件不是服務器端最新的內容。此時我們可以使用協商緩存策略。
協商緩存就是強制緩存失效后,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程。協商緩存可以通過設置兩種 HTTP Header 實現:Last-Modified 和 ETag 。服務器校驗優先考慮Etag。
協商緩存時請求的響應狀態碼是 304,示例如下:

協商緩存生效和失效的請求過程示意圖:
- 協商緩存失效,返回 304 和 Not Modified:

- 協商緩存失效,返回200和請求結果:

5.1、Last-Modified(If-Modified-Since)
瀏覽器在第一次訪問資源時,服務器在返回資源的同時,會自動在 response header 中添加 Last-Modified 的響應頭,值是這個資源在服務器上的最后修改時間,瀏覽器接收后會將該文件響應頭 Last-Modified 緩存起來。
第一次請求:

在瀏覽器下一次請求該資源時,瀏覽器檢測到有 Last-Modified 這個header,會自動往請求頭中添加 If-Modified-Since ,值就是 Last-Modified 中的值:

服務器再次收到這個資源請求時,會自動根據 If-Modified-Since 中的值與服務器中這個資源的最后修改時間對比。如果沒有變化,則返回 304 和 空 的響應體,瀏覽器會直接從緩存讀取該資源。如果 If-Modified-Since 的時間小于服務器中這個資源的最后修改時間,說明文件有更新,服務器會返回新的資源文件和 200。
比如我手動修改了服務器中 index.html 文件中的內容,重新刷新瀏覽器,則結果如下:

可以看到,last-modified 的值自動發生了改變,服務器重新響應了最新的資源。
但是 Last-Modified 存在一些弊端:
- 如果本地打開緩存文件,即使沒有對文件進行修改,但還是會造成 Last-Modified 被修改,服務端不能命中緩存導致發送相同的資源
- 因為 Last-Modified 只能以秒計時,如果在不可感知的時間內修改完成文件,那么服務端會認為資源還是命中了,不會返回正確的資源
5.2、ETag(If-None-Match)
根據文件修改時間來決定是否緩存尚有不足,我們可以直接根據文件內容是否修改來決定緩存策略,也就是使用 ETag。
Etag是服務器響應請求時,返回當前資源文件的一個唯一標識(由服務器生成),只要資源有變化,Etag就會重新生成。瀏覽器在下一次加載資源向服務器發送請求時,會將上一次返回的 Etag 值放到 request header 里的 If-None-Match 里,服務器只需要比較客戶端傳來的If-None-Match 跟自己服務器上該資源的 ETag 是否一致,就能很好地判斷資源相對客戶端而言是否被修改過了。如果服務器發現ETag匹配不上,那么直接以常規GET 200 回包形式將新的資源(當然也包括了新的ETag)發給客戶端;如果ETag是一致的,則直接返回304知會客戶端直接使用本地緩存即可。

5.2.1、Last-Modified和ETag的對比
- 首先在精確度上,Etag要優于Last-Modified。Last-Modified的時間單位是秒,如果某個文件在1秒內改變了多次,那么他們的Last-Modified其實并沒有體現出來修改,但是Etag每次都會改變確保了精度;如果是負載均衡的服務器,各個服務器生成的Last-Modified也有可能不一致。
- 第二在性能上,Etag要遜于Last-Modified,畢竟Last-Modified只需要記錄時間,而Etag需要服務器通過算法來計算出一個hash值。
- 第三在優先級上,服務器校驗優先考慮Etag
6、不同場景推薦使用的緩存策略
6.1、頻繁變動的資源
對于頻繁變動的資源,首先需要使用Cache-Control: no-cache 使瀏覽器每次都請求服務器,然后配合 ETag 或者 Last-Modified 來驗證資源是否有效。這樣的做法雖然不能節省請求數量,但是能顯著減少響應數據大小。
Cache-Control: no-cache
6.2、不常變化的資源
max-age=31536000 (一年),這樣瀏覽器之后請求相同的 URL 會命中強制緩存。而為了解決更新的問題,就需要在文件名(或者路徑)中添加 hash, 版本號等動態字符,之后更改動態字符,從而達到更改引用 URL 的目的,讓之前的強制緩存失效 (其實并未立即失效,只是不再使用了而已)。 在線提供的類庫 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用這個模式。Cache-Control: max-age=31536000
7、用戶行為對瀏覽器緩存的影響
所謂用戶行為對瀏覽器緩存的影響,指的就是用戶在瀏覽器如何操作時,會觸發怎樣的緩存策略。

7.1、瀏覽器中直接輸入url(能命中緩存)
打開網頁,地址欄輸入地址: 能命中緩存。查找 disk cache 中是否有匹配。如有則使用;如沒有則發送網絡請求。
7.2、普通F5刷新(能命中緩存)
普通刷新 (F5):能命中緩存。因為 TAB 并沒有關閉,因此 memory cache 是可用的,會被優先使用(如果匹配的話)。其次才是 disk cache。
7.3、強制刷新 ctrl+F5(不會命中緩存)
- 強制刷新 (Ctrl + F5):不會命中緩存,即瀏覽器不使用緩存。發送的請求頭部均帶有
Cache-control: no-cache(為了兼容,還帶了Pragma: no-cache),服務器直接返回 200 和最新內容。

浙公網安備 33010602011771號