一文讀懂瀏覽器緩存
瀏覽器緩存一直是個老生常談的話題,也是面試官常常用來鑒別面試者的利器,作為前端來講這塊知識是屬于必須掌握的,再者利用好緩存也是做性能優化的有效方法。本文將從緩存原因、緩存讀寫順序,緩存位置以及緩存策略這幾個角度介紹瀏覽器緩存,并且最后給出實踐的應用舉例。

為什么要緩存
很多同學知道緩存的位置和字段,知道怎么用,但是你有沒有想過為什么我們的頁面需要瀏覽器緩存呢?
- 緩存可以減少用戶等待時間,提升用戶體驗,直接從內存或磁盤中取緩存數據肯定是比從服務器請求更快的;
- 減少網絡帶寬消耗:對于網站運營者和用戶,帶寬都代表著成本,過多的帶寬消耗,都需要支付額外的費用。試想一下如果可以使用緩存,只會產生極小的網絡流量,這將有效的降低運營成本。
- 降低服務器壓力:給網絡資源設定有效期之后,用戶可以重復使用本地的緩存,減少對源服務器的請求,降低服務器的壓力。
緩存讀寫順序
當瀏覽器對一個資源(比如一個外鏈的 a.js)進行請求的時候會發生什么?請從緩存的角度大概說下:

1.調用 Service Worker 的 fetch 事件獲取資源;
2.查看 memory cache;
3.查看 disk cache;這里又細分:
- 如果有強制緩存且未失效,則使用強制緩存,不請求服務器。這時的狀態碼全部是 200;
- 如果有強制緩存但已失效,使用協商緩存,比較后確定 304 還是 200;
4.發送網絡請求,等待網絡響應;
5.把響應內容存入 disk cache (如果請求頭信息配置可以存的話);
6.把響應內容的引用存入 memory cache (無視請求頭信息的配置,除了 no-store 之外);
7.把響應內容存入 Service Worker 的 Cache Storage (如果 Service Worker 的腳本調用了 cache.put());
上面這一系列過程其實是瀏覽器查找緩存和把資源存入緩存的執行流程。這其中出現了很多專業詞匯,讓人看了一臉懵逼,下面將從緩存位置和緩存策略兩個角度簡要介紹瀏覽器的緩存。
緩存位置

從瀏覽器開發者工具的 Network 面板下某個請求的 Size 中可以看到當前請求資源的大小以及來源,從這些來源我們就知道該資源到底是從 memory cache中讀取的呢,還是從 disk cache 中讀取的,亦或者是服務器返回的。而這些就是緩存位置:

Service Worker
是一個注冊在指定源和路徑下的事件驅動 worker;特點是:
- 運行在 worker 上下文,因此它不能訪問 DOM;
- 獨立于主線程之外,不會造成阻塞;
- 設計完全異步,所以同步 API(如 XHR 和 localStorage )不能在 Service Worker 中使用;
- 最后處于安全考慮,必須在 HTTPS 環境下才可以使用;
說了這么多特點,那它和緩存有啥關系?其實它有一個功能就是離線緩存:Service Worker Cache;區別于瀏覽器內部的 memory cache 和 disk cache,它允許我們自己去操控緩存,具體操作過程可以參看 Using_Service_Workers;通過 Service Worker 設置的緩存會出現在瀏覽器開發者工具 Application 面板下的 Cache Storage 中。
memory cache
是瀏覽器內存中的緩存,相比于 disk cache 它的特點是讀取速度快,但容量小,且時效性短,一旦瀏覽器 tab 頁關閉,memory cache 就將被清空。memory cache 會自動緩存所有資源嘛?答案肯定是否定的,當 HTTP 頭設置了 Cache-Control: no-store 的時候或者瀏覽器設置了 Disabled cache 就無法把資源存入內存了,其實也無法存入硬盤。當從 memory cache 中查找緩存的時候,不僅僅會去匹配資源的 URL,還會看其 Content-type 是否相同。
disk cache
也叫 HTTP cache 是存在硬盤中的緩存,根據 HTTP 頭部的各類字段進行判定資源的緩存規則,比如是否可以緩存,什么時候過期,過期之后需要重新發起請求嗎?相比于 memory cache 的 disk cache 擁有存儲空間時間長等優點,網站中的絕大多數資源都是存在 disk cache 中的。
- 瀏覽器如何判斷一個資源是存入內存還是硬盤呢?關于這個問題,網上說法不一,不過比較靠譜的觀點是:對于大文件大概率會存入硬盤中;當前系統內存使用率高的話,文件優先存入硬盤。
緩存按照緩存位置劃分,其實還有一個 HTTP/2 的內容 push cache,由于目前國內對 HTTP/2 應用還不廣泛,且網上對 push cache 的知識還不齊全,所以本篇不打算介紹這塊,感興趣的可以閱讀這篇文章:HTTP/2 push is tougher than I thought
緩存策略

根據 HTTP header 的字段又可以將緩存分成強緩存和協商緩存。強緩存可以直接從緩存中讀取資源返回給瀏覽器而不需要向服務器發送請求,而協商緩存是當強緩存失效后(過了過期時間),瀏覽器需要攜帶緩存標識向服務器發送請求,服務器根據緩存標識決定是否使用緩存的過程。強緩存的字段有:Expires 和 Cache-Control。協商緩存的字段有:Last-Modified 和 ETag。
Expires
Expires 是 HTTP/1.0 的字段,表示緩存過期時間,它是一個 GMT 格式的時間字符串。Expires 需要在服務端配置(具體配置也根據服務器而定),瀏覽器會根據該過期日期與客戶端時間對比,如果過期時間還沒到,則會去緩存中讀取該資源,如果已經到期了,則瀏覽器判斷為該資源已經不新鮮要重新從服務端獲取。由于 Expires 是一個絕對的時間,所以會局限于客戶端時間的準確性,從而可能會出現瀏覽器判斷緩存失效的問題。如下是一個 Expires 示例,是一個日期/時間:
- Expires: Wed, 21 Oct 2020 07:28:00 GMT
Cache-Control
它是 HTTP/1.1 的字段,其中的包含的值很多:
- max-age 最大緩存時間,值的單位是秒,在該時間內,瀏覽器不需要向瀏覽器請求。這個設置解決了 Expires 中由于客戶端系統時間不準確而導致緩存失效的問題;
- must-revalidate:如果超過了 max-age 的時間,瀏覽器必須向服務器發送請求,驗證資源是否還有效;
- public 響應可以被任何對象(客戶端、代理服務器等)緩存;
- private 響應只能被客戶端緩存;
- no-cache 跳過強緩存,直接進入協商緩存階段;
- no-store 不緩存任何內容,設置了這個后資源也不會被緩存到內存和硬盤;
Cache-Control 的值是可以混合使用的,比如:
- Cache-Control: private, max-age=0, no-cache
當混合使用的時候它們的優先級如下圖所示:

「當 Expires 和 Cache-Control 都被設置的時候,瀏覽器會優先考慮后者」。當強緩存失效的時候,則會進入到協商緩存階段。具體細節是這樣:瀏覽器從本地查找強緩存,發現失效了,然后會拿著緩存標識請求服務器,服務器拿著這個緩存標識和對應的字段進行校驗資源是否被修改,如果沒有被修改則此時響應狀態會是 304,且不會返回請求資源,而是直接從瀏覽器緩存中讀取。
而瀏覽器緩存標識可以是:Last-Modified 和 ETag:
Last-Modified
資源的最后修改時間;第一次請求的時候,響應頭會返回該字段告知瀏覽器資源的最后一次修改時間;瀏覽器會將值和資源存在緩存中;再次請求該資源的時候,如果強緩存過期,則瀏覽器會設置請求頭的 If-Modifined-Since 字段值為存儲在緩存中的上次響應頭 Last-Modified 的值,并且發送請求;服務器拿著 If-Modifined-Since 的值和 Last-Modified 進行對比。如果相等,表示資源未修改,響應 304;如果不相等,表示資源被修改,響應 200,且返回請求資源。如果資源更新的速度是小于 1 秒的,那么該字段將失效,因為 Last-Modified 時間是精確到秒的。所以有了 ETag。
ETag
根據資源內容生成的唯一標識,資源是否被修改的判斷過程和上面的一致,只是對應的字段替換了。Last-Modified 替換成 ETag,If-Modifined-Since替換成 If-None-Match。
當 Last-Modified 和 ETag 都被設置的時候,瀏覽器會優先考慮后者。
瀏覽器的行為
- 瀏覽器地址欄輸入 URL 后回車:查找 disk cache 中是否有匹配。如有則使用;如沒有則發送網絡請求。
- 普通刷新 (⌘ + R):因為 TAB 頁并沒有關閉,因此 memory cache 是可用的,會被優先使用(如果匹配的話),其次才是 disk cache。
- 強制刷新 (⇧ + ⌘ + R):瀏覽器不使用緩存,因此發送的請求頭部均帶有 Cache-control: no-cache(為了兼容,還帶了 Pragma: no-cache)。服務器直接返回 200 和最新內容。
- 當在開發者工具 Network 面板下設置了 Disabled cache 禁用緩存后,瀏覽器將不會從 memory cache 或者 disk cache 中讀取緩存,而是直接發起網絡請求。
緩存應用
靜態資源
比如頁面引入了一個 JQuery,對于頁面來說這個腳本就是一個工具庫,基本上是不會發生變化的,對于這種資源可以將它的緩存時間設置得長一點,比如如下這個地址的腳本:
- <script src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
你會看到它的響應頭里設置了,max-age=2592000 直接緩存 30 天:
- cache-control: public, max-age=2592000
頻繁變化的資源
對于頻繁變化的資源,比如某個頁面經常需要調整,那么這個頁面就需要在每次請求的時候都進行驗證,可以在響應頭這樣設置:
- cache-control: no-cache
不進行緩存
當然并不是所有請求都能被緩存,無法被瀏覽器緩存的請求如下:
- HTTP 信息頭中包含 Cache-Control: no-cache ,pragma: no-cache(HTTP1.0),或 Cache-Control: max-age=0 等告訴瀏覽器不用緩存的請求;
- 需要根據 Cookie、認證信息等決定輸入內容的動態請求是不能被緩存的;
- 經過 HTTPS 安全加密的請求;
- POST 請求無法被緩存;
- HTTP 響應頭中不包含 Last-Modified/Etag,也不包含 Cache-Control/Expires 的請求無法被緩存;