瀏覽器緩存機制

參考《深入理解瀏覽器的緩存機制》進行整理

前言

緩存可以說是性能優(yōu)化中簡單高效的一種優(yōu)化方式了。一個優(yōu)秀的緩存策略可以縮短網(wǎng)頁請求資源的距離,減少延遲,并且由于緩存文件可以重復利用,還可以減少帶寬,降低網(wǎng)絡(luò)負荷。

對于一個數(shù)據(jù)請求來說,可以分為發(fā)起網(wǎng)絡(luò)請求、后端處理、瀏覽器響應(yīng)三個步驟。瀏覽器緩存可以幫助我們在第一和第三步驟中優(yōu)化性能。比如說直接使用緩存而不發(fā)起請求,或者發(fā)起了請求但后端存儲的數(shù)據(jù)和前端一致,那么就沒有必要再將數(shù)據(jù)回傳回來,這樣就減少了響應(yīng)數(shù)據(jù)。

緩存位置

Service Worker

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

Service Worker 實現(xiàn)緩存功能一般分為三個步驟:首先需要先注冊 Service Worker,然后監(jiān)聽到 install 事件以后就可以緩存需要的文件,那么在下次用戶訪問的時候就可以通過攔截請求的方式查詢是否存在緩存,存在緩存的話就可以直接讀取緩存文件,否則就去請求數(shù)據(jù)。

當 Service Worker 沒有命中緩存的時候,我們需要去調(diào)用 fetch 函數(shù)獲取數(shù)據(jù)。也就是說,如果我們沒有在 Service Worker 命中緩存的話,會根據(jù)緩存查找優(yōu)先級去查找數(shù)據(jù)。但是不管我們是從 Memory Cache 中還是從網(wǎng)絡(luò)請求中獲取的數(shù)據(jù),瀏覽器都會顯示我們是從 Service Worker 中獲取的內(nèi)容。

Memory Cache

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

那么既然內(nèi)存緩存這么高效,我們是不是能讓數(shù)據(jù)都存放在內(nèi)存中呢?這是不可能的。計算機中的內(nèi)存一定比硬盤容量小得多,操作系統(tǒng)需要精打細算內(nèi)存的使用,所以能讓我們使用的內(nèi)存必然不多。

當我們訪問過頁面以后,再次刷新頁面,可以發(fā)現(xiàn)很多數(shù)據(jù)都來自于內(nèi)存緩存。

?
image
                                       ?

內(nèi)存緩存中有一塊重要的緩存資源是 preloader 相關(guān)指令(例如<link rel="prefetch">)下載的資源。眾所周知 preloader 的相關(guān)指令已經(jīng)是頁面優(yōu)化的常見手段之一,它可以一邊解析 js/css 文件,一邊網(wǎng)絡(luò)請求下一個資源。

需要注意的事情是,內(nèi)存緩存在緩存資源時并不關(guān)心返回資源的 HTTP 緩存頭 Cache-Control 是什么值,同時資源的匹配也并非僅僅是對 URL 做匹配,還可能會對 Content-Type,CORS 等其他特征做校驗。

Disk Cache

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

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

瀏覽器會把哪些文件丟進內(nèi)存中?哪些丟進硬盤中?關(guān)于這點網(wǎng)上說法不一,不過以下觀點比較靠得?。?/p>

  • 對于大文件來說,大概率是不存儲在內(nèi)存中的,反之優(yōu)先;
  • 當前系統(tǒng)內(nèi)存使用率高的話,文件優(yōu)先存儲進硬盤。

Push Cache

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

Push Cache 在國內(nèi)能夠查到的資料很少,也是因為 HTTP/2 在國內(nèi)不夠普及。這里推薦閱讀Jake Archibald的 HTTP/2 push is tougher than I thought 這篇文章,文章中的幾個結(jié)論:

  • 所有的資源都能被推送,并且能夠被緩存, 但是 Edge 和 Safari 瀏覽器支持相對比較差;
  • 可以推送 no-cache 和 no-store 的資源;
  • 一旦連接被關(guān)閉,Push Cache 就被釋放;
  • 多個頁面可以使用同一個 HTTP/2 的連接,也就可以使用同一個 Push Cache。這主要還是依賴瀏覽器的實現(xiàn)而定,出于對性能的考慮,有的瀏覽器會對相同域名但不同的 tab 標簽使用同一個 HTTP 連接;
  • Push Cache 中的緩存只能被使用一次;
  • 瀏覽器可以拒絕接受已經(jīng)存在的資源推送;
  • 你可以給其他域名推送資源。

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

那么為了性能上的考慮,大部分的接口都應(yīng)該選擇好緩存策略,通常瀏覽器緩存策略分為兩種:強緩存和協(xié)商緩存,并且緩存策略都是通過設(shè)置 HTTP Header 來實現(xiàn)的。

幾種緩存資源size對比

?

<colgroup style="box-sizing: border-box;"><col span="1" width="239" style="box-sizing: border-box;"><col span="1" width="240" style="box-sizing: border-box;"><col span="1" width="240" style="box-sizing: border-box;"></colgroup>
| 狀態(tài) | size | 說明 |
| 200 | form memory cache | 不請求網(wǎng)絡(luò)資源,資源在內(nèi)存當中,一般腳本、字體、圖片會存在內(nèi)存當中 |
| 200 | form disk ceche | 不請求網(wǎng)絡(luò)資源,在磁盤當中,一般非腳本會存在內(nèi)存當中,如css等 |
| 200 | 資源大小數(shù)值 | 從服務(wù)器下載最新資源 |
| 304 | 報文大小 | 請求服務(wù)端發(fā)現(xiàn)資源沒有更新,使用本地資源 |

?

緩存過程分析

瀏覽器與服務(wù)器通信的方式為應(yīng)答模式,即是:瀏覽器發(fā)起 HTTP 請求 – 服務(wù)器響應(yīng)該請求,那么瀏覽器怎么確定一個資源該不該緩存,如何去緩存呢?瀏覽器第一次向服務(wù)器發(fā)起該請求后拿到請求結(jié)果后,將請求結(jié)果和緩存標識存入瀏覽器緩存,瀏覽器對于緩存的處理是根據(jù)第一次請求資源時返回的響應(yīng)頭來確定的。具體過程如下圖:

?
image
                                       ?

由上圖我們可以知道:

  • 瀏覽器每次發(fā)起請求,都會先在瀏覽器緩存中查找該請求的結(jié)果以及緩存標識;
  • 瀏覽器每次拿到返回的請求結(jié)果都會將該結(jié)果和緩存標識存入瀏覽器緩存中。

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

強制緩存

強緩存:不會向服務(wù)器發(fā)送請求,直接從緩存中讀取資源,在 chrome 控制臺的 Network 選項中可以看到該請求返回 200 的狀態(tài)碼,并且 Size 顯示 from disk cache 或 from memory cache。強緩存可以通過設(shè)置兩種 HTTP Header 實現(xiàn):Expires 和 Cache-Control。

Expires

緩存過期時間,用來指定資源到期的時間,是服務(wù)器端的具體的時間點(絕對時間)。也就是說,Expires=格林喬治時間,需要和 Last-modified 結(jié)合使用。Expires 是 Web 服務(wù)器響應(yīng)消息頭字段,在響應(yīng) http 請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數(shù)據(jù),而無需再次請求。

Expires 是 HTTP/1 的產(chǎn)物,受限于本地時間,如果修改了本地時間,或者受時區(qū)影響,可能會造成緩存失效。Expires: Wed, 22 Oct 2018 08:41:00 GMT表示資源會在這個時間點后過期,需要再次請求。

Cache-Control

在 HTTP/1.1 中,Cache-Control 是最重要的規(guī)則,主要用于控制網(wǎng)頁緩存。比如當Cache-Control:max-age=300時,則代表在這個請求正確返回時間(瀏覽器也會記錄下來)的 5 分鐘內(nèi)再次加載資源,就會命中強緩存。優(yōu)先級比Expires 高。

格式示例:Cache-Control: public, max-age=31536000

Cache-Control 可以在請求頭或者響應(yīng)頭中設(shè)置,并且可以組合使用多種指令:

指令 作用
public 表示響應(yīng)可以被客戶和代理服務(wù)器緩存
private 表示響應(yīng)只可以被客戶端緩存
max-age=30 緩存 30 秒后就過期,需要重新請求
s-maxage=30 覆蓋 max-age ,作用一樣,只在代理服務(wù)器中生效
no-store 不緩存任何響應(yīng)
no-cache 資源被緩存,但是強制緩存立即失效,下次會發(fā)起請求驗證資源是否過期
max-stale=30 30 秒內(nèi),及時緩存過期,也使用該緩存
min-fresh=30 希望在 30 秒內(nèi)獲取最新的響應(yīng)

?

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

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

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

no-store:所有內(nèi)容都不會被緩存,即不使用強制緩存,也不使用協(xié)商緩存

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

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

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

min-fresh:能夠容忍的最小新鮮度。min-fresh 標示了客戶端不愿意接受新鮮度不多于當前的 age 加上 min-fresh 設(shè)定的時間之和的響應(yīng)。

?
image
                                       ?

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

Expires 和 Cache-Control 兩者對比

其實這兩者差別不大,區(qū)別就在于 Expires 是 http1.0 的產(chǎn)物,Cache-Control 是 http1.1 的產(chǎn)物,兩者同時存在的話,Cache-Control 優(yōu)先級高于 Expires;在某些不支持 HTTP1.1 的環(huán)境下,Expires 就會發(fā)揮用處。所以 Expires 其實是過時的產(chǎn)物,現(xiàn)階段它的存在只是一種兼容性的寫法。

強緩存判斷是否緩存的依據(jù)來自于是否超出某個時間或者某個時間段,而不關(guān)心服務(wù)器端文件是否已經(jīng)更新,這可能會導致加載文件不是服務(wù)器端最新的內(nèi)容,那我們?nèi)绾潍@知服務(wù)器端內(nèi)容是否已經(jīng)發(fā)生了更新呢?此時我們需要用到協(xié)商緩存策略。

協(xié)商緩存

協(xié)商緩存就是強制緩存失效后,瀏覽器攜帶緩存標識向服務(wù)器發(fā)起請求,由服務(wù)器根據(jù)緩存標識決定是否使用緩存的過程,主要有以下兩種情況:

  • 協(xié)商緩存生效,返回 304 和 Not Modified

?
image
                                       ?
  • 協(xié)商緩存失效,返回 200 和請求結(jié)果

?
image
                                       ?

協(xié)商緩存可以通過設(shè)置兩種 HTTP Header 實現(xiàn):Last-Modified 和 ETag 。

Last-Modified 和 If-Modified-Since

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

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

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

?
image

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

  • 如果本地打開緩存文件,即使沒有對文件進行修改,但還是會造成 Last-Modified 被修改,服務(wù)端不能命中緩存導致發(fā)送相同的資源;
  • 因為 Last-Modified 只能以秒計時,如果在不可感知的時間內(nèi)修改完成文件,那么服務(wù)端會認為資源還是命中了,不會返回正確的資源。

既然根據(jù)文件修改時間來決定是否緩存尚有不足,能否可以直接根據(jù)文件內(nèi)容是否修改來決定緩存策略?所以在 HTTP / 1.1 出現(xiàn)了 ETag 和If-None-Match。

ETag 和 If-None-Match

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

?
image

注意:在分布式系統(tǒng)中,相同的資源在不同服務(wù)器返回的ETag會是不同的

兩者之間對比

  • 首先在精確度上,Etag 要優(yōu)于 Last-Modifie,Last-Modified 的時間單位是秒,如果某個文件在 1 秒內(nèi)改變了多次,那么他們的 Last-Modified 其實并沒有體現(xiàn)出來修改,但是 Etag 每次都會改變確保了精度;如果是負載均衡的服務(wù)器,各個服務(wù)器生成的 Last-Modified 也有可能不一致;
  • 第二在性能上,Etag 要遜于 Last-Modified,畢竟 Last-Modified 只需要記錄時間,而 Etag 需要服務(wù)器通過算法來計算出一個 hash 值, 更耗時;
  • 第三在優(yōu)先級上,默認情況下ETag優(yōu)先級高于Last-Modifyed,服務(wù)器也可以根據(jù)自己的緩存機制需求,選擇以哪一個作為判斷依據(jù)。

緩存機制

?
image

簡要描述這個過程:

  1. 第一次請求,無緩存,直接向服務(wù)器發(fā)請求,并將請求結(jié)果存入緩存中;
  2. 第二次請求,在強制緩存時間內(nèi),緩存未過期,瀏覽器接使用緩存作為結(jié)果返回 200;
  3. 第三次請求,強制緩存時間已過期,進入?yún)f(xié)商緩存,向服務(wù)器請求,通過在Header中攜帶 If-Modified-Since(對應(yīng)瀏覽器返回的last-Modify) 或If-None-Match(對應(yīng)瀏覽器返回的Etag) 校驗緩存內(nèi)容是否有更新,Etag優(yōu)先級更高,請求結(jié)果分兩種情況:
  • 緩存資源沒有更新,返回 304,瀏覽器繼續(xù)使用緩存,更新強制緩存時間
  • 緩存資源有更新,緩存失效,返回 200,重新返回資源和緩存標識,再存入瀏覽器緩存中

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

對于這種情況,瀏覽器會采用一個啟發(fā)式的算法,通常會取響應(yīng)頭中的 Date 減去 Last-Modified 值的 10% 作為緩存時間。

七、實際場景應(yīng)用緩存策略

頻繁變動的資源

Cache-Control: no-cache

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

不常變化的資源

Cache-Control: max-age=31536000

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

用戶行為對瀏覽器緩存的影響

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

  • 打開網(wǎng)頁,地址欄輸入地址: 查找 disk cache 中是否有匹配。如有則使用;如沒有則發(fā)送網(wǎng)絡(luò)請求;
  • 普通刷新 (F5):因為 TAB 并沒有關(guān)閉,因此 memory cache 是可用的,會被優(yōu)先使用 (如果匹配的話)。其次才是 disk cache;
  • 強制刷新 (Ctrl + F5):瀏覽器不使用緩存,因此發(fā)送的請求頭部均帶有 Cache-control: no-cache(為了兼容,還帶了 Pragma: no-cache), 服務(wù)器直接返回 200 和最新內(nèi)容。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容