瀏覽器緩存類型
瀏覽器緩存的作用和好處無需多言,其根據是否需要向服務器重新發起 http 請求分為強緩存和協商緩存兩種類型。強緩存的優先級要高於協商緩存。
強緩存
強緩存機制下瀏覽器首先查找本地緩存,如果命中則不會向服務器發起請求。此時返回 200 狀態碼,並帶有 from disk cache 或 from memory cache 字樣。強緩存可以通過設置兩種 HTTP Header 實現:Expires 和 Cache-Control。
Memory Cache
Memory Cache 也就是內存中的緩存,主要包含的是當前頁面中已經獲取到的資源,比如頁面上已經下載的樣式、腳本、圖片等。讀取內存中的數據肯定比磁盤快,內存緩存雖然讀取高效,可是緩存持續很短,會隨着進程的釋放而釋放。一旦我們關閉 Tab 頁面,內存中的緩存也就被釋放了。既然內存緩存這麼高效,我們是不是能讓數據都存放在內存中呢?這是不可能的。計算機中的內存一定比硬盤容量小得多,操作系統需要精打細算內存的使用,所以能讓我們使用的內存必然不多。
Disk Cache
Disk Cache 也就是存儲在磁盤中的緩存,讀取速度慢點,但是什麼都能存儲到磁盤中,比之 Memory Cache 勝在容量和存儲時效性上。在所有瀏覽器緩存中,Disk Cache 覆蓋面基本是最大的。它會根據 HTTP Herder 中的字段判斷哪些資源需要緩存,哪些資源可以不請求直接使用,哪些資源已經過期需要重新請求。絕大部分的緩存都來自 Disk Cache。
瀏覽器會把哪些文件丟進內存中?哪些丟進磁盤中?關於這點,網上説法不一,不過以下觀點比較靠得住:
- 對於大文件來説,大概率是不存儲在內存中的,反之優先;
- 當前系統內存使用率高的話,文件優先存儲進磁盤。
Expires
Expires 用來指定緩存過期時間,是服務器端發的具體時間點。也就是説,Expires=請求時間 + max-age。Expires 是服務器響應消息頭字段,在響應 http 請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存讀取數據,而無需再次請求。
Expires 是 HTTP/1.0 的產物,受限於本地時間。如果修改了本地時間,可能會造成緩存失效。比如 Expires: Wed, 22 Oct 2018 08:30:00 GMT表示資源會在 Wed, 22 Oct 2018 08:30:00 GMT 後過期,需要再次請求。
Cache-Control
在 HTTP/1.1中,Cache-Control 是最重要的規則,主要用於控制網頁緩存。比如當 Cache-Control:max-age=300時,則代表在這個請求正確返回時間(瀏覽器會記錄下來)的5分鐘內再次加載資源,就會命中強緩存。我們重點看看如下幾個設置值:
public
所有內容都將被緩存(客户端和代理服務器都可緩存)。具體來説響應可被任何中間節點緩存,如 Browser <-- proxy <-- Server,中間的 proxy 可以緩存資源,比如下次再請求同一資源時 proxy 直接把自己緩存的內容給 Browser 而不再向 Server 要。
private
所有內容只有客户端可以緩存,Cache-Control 的默認取值。具體來説,表示中間節點不允許緩存,對於 Browser <-- proxy <-- Server,proxy 會老老實實把 Server Browser,自己不緩存任何數據。當下次 Browser 再次請求時 proxy 會做好請求轉發而不是自作主張給自己緩存的數據。
no-cache
客户端緩存內容,是否使用緩存則需要經過協商緩存來驗證決定。表示不使用 Cache-Control 的緩存控制方式做前置驗證,而是使用 Etag 或者 Last-Modified 字段來控制緩存。需要注意的是,no-cache 這個名字有一點誤導。設置了 no-cache 之後,並不是説瀏覽器就不再緩存數據,只是瀏覽器在使用緩存數據時,需要先確認一下數據是否還跟服務器保持一致。
no-store
所有內容都不會被緩存,既不使用強緩存,也不使用協商緩存。
max-age
max-age=xxx(xxx is numeric) 表示緩存內容將在 xxx 秒後失效。
Expires 和 Cache-Control 兩者對比
其實這兩者差別不大,區別就在於 Expires 是 HTTP/1.0 的產物,Cache-Control 是 HTTP/1.1 的產物。兩者同時存在的話,Cache-Control 優先級高於 Expires。在某些不支持 HTTP1.1 的環境下,Expires 就會發揮用處。所以 Expires 其實是過時的產物,現階段它的存在只是一種兼容性的寫法。
強緩存判斷是否緩存的依據來自於是否超出某個時間或者某個時間段,而不關心服務器端文件是否已經更新,這可能會導致加載文件不是服務器端最新的內容,那我們如何獲知服務器端內容是否已經發生了更新呢?此時我們需要用到協商緩存。
協商緩存
協商緩存就是強緩存失效後,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程,主要有以下兩種情況:
- 協商緩存生效,返回 304 和 Not Modified。
- 協商緩存失效,返回 200 和請求結果。
協商緩存可以通過設置兩種 HTTP Header 實現:Last-Modified 和 ETag。
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。
ETag 和 If-None-Match
Etag 是服務器響應請求時返回當前資源文件的一個唯一標識(由服務器生成),只要資源有變化,Etag 就會重新生成。瀏覽器在下一次加載資源向服務器發送請求時,會將上一次返回的 Etag 值放到 request header 裏的 If-None-Match 裏,服務器只需要比較客户端傳來的 If-None-Match 跟自己服務器上該資源的 ETag 是否一致,就能很好地判斷資源相對客户端而言是否被修改過了。如果服務器發現 ETag 匹配不上,那麼直接以常規 GET 200 回包形式將新的資源(當然也包括了新的 ETag)發給客户端。如果 ETag 是一致的,則返回 304 指示客户端直接使用本地緩存即可。
Last-Modified 和 ETag 兩者對比
- 在精確度上,Etag 要優於 Last-Modified。Last-Modified 的時間單位是秒,如果某個文件在 1 秒內改變了多次,那麼他們的 Last-Modified 其實並沒有體現出來修改。但是 Etag 每次都會改變確保了精度。如果是負載均衡的服務器,各個服務器生成的 Last-Modified 也有可能不一致。
- 在性能上,Etag 要遜於 Last-Modified,畢竟 Last-Modified 只需要記錄時間,而 Eta g需要服務器通過算法來計算出一個 hash 值。
- 在優先級上,服務器校驗優先考慮 Etag。
緩存機制
強緩存優先級高於協商緩存,若強緩存(Expires 和 Cache-Control)生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match)。協商緩存由服務器決定是否使用緩存,若協商緩存失效,那麼代表該請求的緩存失效,返回 200,重新返回資源和緩存標識,再存入瀏覽器緩存中;生效則返回 304,繼續使用緩存。
那如果什麼緩存策略都沒設置,瀏覽器會怎麼處理呢?對於這種情況,瀏覽器會採用一個啓發式的算法,通常會取響應頭中的 Date 減去 Last-Modified 值的 10% 作為緩存時間。
前端 SPA 應用緩存策略
首頁 index.html 緩存策略
由於默認什麼都不設置的情況下會應用強緩存,這樣就會導致每次 index.html 有更新時用户端都不一定能及時得到最新版本(經常需要用户強制刷新或手動清除瀏覽器緩存)。建議 index.html 中設置 Cache-Control 值為 no-cache 取消強緩存,使瀏覽器每次都請求服務器,然後配合 ETag 或者 Last-Modified 來協商驗證資源是否有效。這樣的做法雖然不能節省請求數量,但是能顯著減少響應數據大小且兼顧了及時更新的需求,另外由於只有 index.html 這一個單頁需要這樣,因此這種方式明顯利大於弊。
靜態資源文件緩存策略
靜態資源文件如 css js 文件等,由於前端構建打包輸出中對於這些文件會引入 hash 動態值,那麼每次有更新時加載他們的 URL 也都不相同。因此對於這些文件,我們完全可以設置強緩存 Cache-Control 為一個很大的值,比如半年或一年(以 nginx 舉例來説,可以設置 expires 180d 等),在此期間內瀏覽器無需發起任何請求,直接使用本地緩存,最大化利用瀏覽器緩存。而在這些文件有更新的時候,由於請求 URL 也變了,所以無需擔心瀏覽器不能及時得到最新版本。