瀏覽器http緩存機制

前言

Http 緩存機制作爲 web 性能優化的重要手段,對於從事 Web 開發的同學們來說,應該是知識體系庫中的一個基礎環節,同時對於有志成爲前端架構師的同學來說是必備的知識技能。
但是對於很多前端同學來說,僅僅只是知道瀏覽器會對請求的靜態文件進行緩存,但是爲什麼被緩存,緩存是怎樣生效的,卻並不是很清楚。
在此,我會嘗試用簡單明瞭的文字,像大家系統的介紹HTTP緩存機制,期望對各位正確的理解前端緩存有所幫助。


在介紹HTTP緩存之前,作爲知識鋪墊,先簡單介紹一下HTTP報文

HTTP報文就是瀏覽器和服務器間通信時發送及響應的數據塊。
瀏覽器向服務器請求數據,發送請求(request)報文;服務器向瀏覽器返回數據,返回響應(response)報文。
報文信息主要分爲兩部分
1.包含屬性的首部(header)--------------------------附加信息(cookie,緩存信息等)與緩存相關的規則信息,均包含在header中
2.包含數據的主體部分(body)-----------------------HTTP請求真正想要傳輸的部分

http報文中與緩存相關的首部字段

我們先來瞅一眼RFC2616規定的47種http報文首部字段中與緩存相關的字段,事先了解一下能讓咱在心裏有個底:

1. 通用首部字段(就是請求報文和響應報文都能用上的字段)

2. 請求首部字段

3. 響應首部字段

4. 實體首部字段

緩存規則解析

爲方便大家理解,我們認爲瀏覽器存在一個緩存數據庫,用於存儲緩存信息。
在客戶端第一次請求數據時,此時緩存數據庫中沒有對應的緩存數據,需要請求服務器,服務器返回後,將數據存儲至緩存數據庫中。

HTTP緩存有多種規則,根據是否需要重新向服務器發起請求來分類,我將其分爲兩大類(強制緩存,對比緩存)
在詳細介紹這兩種規則之前,先通過時序圖的方式,讓大家對這兩種規則有個簡單瞭解。

已存在緩存數據時,僅基於強制緩存,請求數據的流程如下


已存在緩存數據時,僅基於對比緩存,請求數據的流程如下

對緩存機制不太瞭解的同學可能會問,基於對比緩存的流程下,不管是否使用緩存,都需要向服務器發送請求,那麼還用緩存幹什麼?
這個問題,我們暫且放下,後文在詳細介紹每種緩存規則的時候,會帶給大家答案。

我們可以看到兩類緩存規則的不同,強制緩存如果生效,不需要再和服務器發生交互,而對比緩存不管是否生效,都需要與服務端發生交互。
兩類緩存規則可以同時存在,強制緩存優先級高於對比緩存,也就是說,當執行強制緩存的規則時,如果緩存生效,直接使用緩存,不再執行對比緩存規則。


強制緩存

從上文我們得知,強制緩存,在緩存數據未失效的情況下,可以直接使用緩存數據,那麼瀏覽器是如何判斷緩存數據是否失效呢?
我們知道,在沒有緩存數據的時候,瀏覽器向服務器請求數據時,服務器會將數據和緩存規則一併返回,緩存規則信息包含在響應header中

對於強制緩存來說,響應header中會有兩個字段來標明失效規則(Expires/Cache-Control
使用chrome的開發者工具,可以很明顯的看到對於強制緩存生效時,網絡請求的情況


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

http1.0 時代,給客戶端設定緩存方式可通過兩個字段——“Pragma”和“Expires”來規範。雖然這兩個字段早可拋棄,但爲了做http協議的向下兼容,你還是可以看到很多網站依舊會帶上這兩個字段。

Pragma

當該字段值爲“no-cache”的時候(事實上現在RFC中也僅標明該可選值),會知會客戶端不要對該資源讀緩存,即每次都得向服務器發一次請求才行

Pragma屬於通用首部字段,在客戶端上使用時,常規要求我們往html上加上這段meta元標籤(而且可能還得做些hack放到body後面去):

<meta http-equiv="Pragma" content="no-cache">

告訴瀏覽器每次請求頁面時都不要讀緩存,都得往服務器發一次請求才行。

BUT!!! 事實上這種禁用緩存的形式用處很有限:

1. 僅有IE才能識別這段meta標籤含義,其它主流瀏覽器僅能識別“Cache-Control: no-store”的meta標籤(見出處
2. 在IE中識別到該meta標籤含義,並不一定會在請求字段加上Pragma,但的確會讓當前頁面每次都發新請求(僅限頁面,頁面上的資源則不受影響)

做了測試後發現也的確如此,這種客戶端定義Pragma的形式基本沒起到多少作用。

不過如果是在響應報文上加上該字段就不一樣了:

如上圖紅框部分是再次刷新頁面時生成的請求,這說明禁用緩存生效,預計瀏覽器在收到服務器的Pragma字段後會對資源進行標記,禁用其緩存行爲,進而後續每次刷新頁面均能重新發出請求而不走緩存。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Expires
  Expires的值爲服務端返回的到期時間,即下一次請求時,請求時間小於服務端返回的到期時間,直接使用緩存數據。
不過Expires 是HTTP 1.0的東西,現在默認瀏覽器均默認使用HTTP 1.1,所以它的作用基本忽略。

       Expires的值對應一個GMT(格林尼治時間),比如“Mon, 22 Jul 2002 11:12:01 GMT”來告訴瀏覽器資源緩存過期時間,如果還沒過該時間點則不發請求。

在客戶端我們同樣可以使用meta標籤來知會IE(也僅有IE能識別)頁面(同樣也只對頁面有效,對頁面上的資源無效)緩存時間:

<meta http-equiv="expires" content="mon, 18 apr 2016 14:30:00 GMT">

如果希望在IE下頁面不走緩存,希望每次刷新頁面都能發新請求,那麼可以把“content”裏的值寫爲“-1”或“0”。

注意的是該方式僅僅作爲知會IE緩存時間的標記,你並不能在請求或響應報文中找到Expires字段。

如果是在服務端報頭返回Expires字段,則在任何瀏覽器中都能正確設置資源緩存的時間:

在上圖裏,緩存時間設置爲一個已過期的時間點(見紅框),則刷新頁面將重新發送請求(見藍框)

那麼如果Pragma和Expires一起上陣的話,聽誰的?我們試一試就知道了:

我們通過Pragma禁用緩存,又給Expires定義一個還未到期的時間(紅框),刷新頁面時發現均發起了新請求(藍框),這意味着Pragma字段的優先級會更高

BUT,響應報文中Expires所定義的緩存時間是相對服務器上的時間而言的,如果客戶端上的時間跟服務器上的時間不一致(特別是用戶修改了自己電腦的系統時間),那緩存時間可能就沒啥意義了(到期時間是由服務端生成的,但是客戶端時間可能跟服務端時間有誤差,這就會導致緩存命中的誤差)。
所以HTTP 1.1 的版本,使用Cache-Control替代。

Cache-Control

針對上述的“Expires時間是相對服務器而言,無法保證和客戶端時間統一”的問題,http1.1新增了 Cache-Control 來定義緩存過期時間,若報文中同時出現了 Pragma、Expires 和 Cache-Control,會以 Cache-Control 爲準。

Cache-Control也是一個通用首部字段,這意味着它能分別在請求報文和響應報文中使用。在RFC中規範了 Cache-Control 的格式爲:

"Cache-Control" ":" cache-directive

作爲請求首部時,cache-directive 的可選值有:

作爲響應首部時,cache-directive 的可選值有:

我們依舊可以在HTML頁面加上meta標籤來給請求報頭加上 Cache-Control 字段:

另外 Cache-Control 允許自由組合可選值,例如:

Cache-Control: max-age=3600, must-revalidate

它意味着該資源是從原服務器上取得的,且其緩存(新鮮度)的有效時間爲一小時,在後續一小時內,用戶重新訪問該資源則無須發送請求。

當然這種組合的方式也會有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。

組合的形式還能做一些瀏覽器行爲不一致的兼容處理。例如在IE我們可以使用 no-cache 來防止點擊“後退”按鈕時頁面資源從緩存加載,但在 Firefox 中,需要使用 no-store 才能防止歷史回退時瀏覽器不從緩存中去讀取數據,故我們在響應報頭加上如下組合值即可做兼容處理:

Cache-Control: no-cache, no-store


Cache-Control 是最重要的規則。常見的取值有private、public、no-cache、max-age,no-store,默認爲private。
private:             客戶端可以緩存
public:              客戶端和代理服務器都可緩存(前端的同學,可以認爲public和private是一樣的)
max-age=xxx:   緩存的內容將在 xxx 秒後失效
no-cache:          需要使用對比緩存來驗證緩存數據(後面介紹)
no-store:           所有內容都不會緩存,強制緩存,對比緩存都不會觸發(對於前端開發來說,緩存越多越好,so...基本上和它說886)

舉個板栗

圖中Cache-Control僅指定了max-age,所以默認爲private,緩存時間爲31536000秒(365天)
也就是說,在365天內再次請求這條數據,都會直接獲取緩存數據庫中的數據,直接使用。

對比緩存

對比緩存,顧名思義,需要進行比較判斷是否可以使用緩存
瀏覽器第一次請求數據時,服務器會將緩存標識與數據一起返回給客戶端,客戶端將二者備份至緩存數據庫中。
再次請求數據時,客戶端將備份的緩存標識發送給服務器,服務器根據緩存標識進行判斷,判斷成功後,返回304狀態碼,通知客戶端比較成功,可以使用緩存數據。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

緩存校驗字段

上述的首部字段均能讓客戶端決定是否向服務器發送請求,比如設置的緩存時間未過期,那麼自然直接從本地緩存取數據即可(在chrome下表現爲200 from cache),若緩存時間過期了或資源不該直接走緩存,則會發請求到服務器去

我們現在要說的問題是,如果客戶端向服務器發了請求,那麼是否意味着一定要讀取回該資源的整個實體內容呢?

我們試着這麼想——客戶端上某個資源保存的緩存時間過期了,但這時候其實服務器並沒有更新過這個資源,如果這個資源數據量很大,客戶端要求服務器再把這個東西重新發一遍過來,是否非常浪費帶寬和時間呢?

答案是肯定的,那麼是否有辦法讓服務器知道客戶端現在存有的緩存文件,其實跟自己所有的文件是一致的,然後直接告訴客戶端說“這東西你直接用緩存裏的就可以了,我這邊沒更新過呢,就不再傳一次過去了”。

爲了讓客戶端與服務器之間能實現緩存文件是否更新的驗證、提升緩存的複用率,Http1.1新增了幾個首部字段來做這件事情:

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
第一次訪問:

再次訪問:

通過兩圖的對比,我們可以很清楚的發現,在對比緩存生效時,狀態碼爲304,並且報文大小和請求時間大大減少。
原因是,服務端在進行標識比較後,只返回header部分,通過狀態碼通知客戶端使用緩存不再需要將報文主體部分返回給客戶端

對於對比緩存來說,緩存標識的傳遞是我們着重需要理解的,它在請求header和響應header間進行傳遞
一共分爲兩種標識傳遞,接下來,我們分開介紹。


1、Last-Modified  /  If-Modified-Since
Last-Modified:
服務器在響應請求時,告訴瀏覽器資源的最後修改時間

 

服務器將資源傳遞給客戶端時,會將資源最後更改的時間以“Last-Modified: GMT”的形式加在實體首部上一起返回給客戶端。

客戶端會爲資源標記上該信息,下次再次請求時,會把該信息附帶在請求報文中一併帶給服務器去做檢查,若傳遞的時間值與服務器上該資源最終修改時間是一致的,則說明該資源沒有被修改過,直接返回304狀態碼即可。

至於傳遞標記起來的最終修改時間的請求報文首部字段一共有兩個:

⑴ If-Modified-Since:Last-Modified-value
再次請求服務器時,通過此字段通知服務器:上次請求時服務器返回的資源最後修改時間
服務器收到請求後發現有頭If-Modified-Since 則與被請求資源的最後修改時間進行比對
若資源的最後修改時間大於If-Modified-Since,說明資源又被改動過,則響應整片資源內容,返回狀態碼200
若資源的最後修改時間小於或等於If-Modified-Since,說明資源無新修改,則響應HTTP 304告知瀏覽器繼續使用所保存的cache。

當前各瀏覽器均是使用的該請求首部來向服務器傳遞保存的 Last-Modified 值。

 If-Unmodified-Since: Last-Modified-value

告訴服務器,若Last-Modified沒有匹配上(資源在服務端的最後更新時間改變了),則應當返回412(Precondition Failed) 狀態碼給客戶端。

當遇到下面情況時,If-Unmodified-Since 字段會被忽略:

1. Last-Modified值對上了(資源在服務端沒有新的修改);
2. 服務端需返回2XX和412之外的狀態碼;
3. 傳來的指定日期不合法

Last-Modified 說好卻也不是特別好,因爲如果在服務器上,一個資源被修改了,但其實際內容根本沒發生改變,會因爲Last-Modified時間匹配不上而返回了整個實體給客戶端(即使客戶端緩存裏有個一模一樣的資源)

2、Etag  /  If-None-Match優先級高於Last-Modified  /  If-Modified-Since)
Etag:

服務器響應請求時,告訴瀏覽器當前資源在服務器的唯一標識(生成規則由服務器決定)。

爲了解決上述Last-Modified可能存在的不準確的問題,Http1.1還推出了 ETag 實體首部字段。

服務器會通過某種算法,給資源計算得出一個唯一標誌符(比如md5標誌),在把資源響應給客戶端的時候,會在實體首部加上“ETag: 唯一標識符”一起返回給客戶端。

客戶端會保留該 ETag 字段,並在下一次請求時將其一併帶過去給服務器。服務器只需要比較客戶端傳來的ETag跟自己服務器上該資源的ETag是否一致,就能很好地判斷資源相對客戶端而言是否被修改過了。

如果服務器發現ETag匹配不上,那麼直接以常規GET 200回包形式將新的資源(當然也包括了新的ETag)發給客戶端;如果ETag是一致的,則直接返回304知會客戶端直接使用本地緩存即可。

那麼客戶端是如何把標記在資源上的 ETag 傳去給服務器的呢?請求報文中有兩個首部字段可以帶上 ETag 值:

(1)  If-None-Match:
再次請求服務器時,通過此字段通知服務器客戶段緩存數據的唯一標識
服務器收到請求後發現有頭If-None-Match 則與被請求資源的唯一標識進行比對
不同,說明資源又被改動過,則響應整片資源內容,返回狀態碼200
相同,說明資源無新修改,則響應HTTP 304,告知瀏覽器繼續使用所保存的cache

⑵ If-Match: ETag-value

告訴服務器如果沒有匹配到ETag,或者收到了“*”值而當前並沒有該資源實體,則應當返回412(Precondition Failed) 狀態碼給客戶端。否則服務器直接忽略該字段。

If-Match 的一個應用場景是,客戶端走PUT方法向服務端請求上傳/更替資源,這時候可以通過 If-Match 傳遞資源的ETag。

 

需要注意的是,如果資源是走分佈式服務器(比如CDN)存儲的情況,需要這些服務器上計算ETag唯一值的算法保持一致,纔不會導致明明同一個文件,在服務器A和服務器B上生成的ETag卻不一樣。

如果 Last-Modified 和 ETag 同時被使用,則要求它們的驗證都必須通過纔會返回304,若其中某個驗證沒通過,則服務器會按常規返回資源實體及200狀態碼。

總結
對於強制緩存,服務器通知瀏覽器一個緩存時間在緩存時間內,下次請求,直接用緩存,不在時間內,執行比較緩存策略
對於比較緩存將緩存信息中的Etag和Last-Modified通過請求發送給服務器,由服務器校驗,返回304狀態碼時,瀏覽器直接使用緩存。


瀏覽器第一次請求:


瀏覽器再次請求時:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章