天天看點

一篇文章,徹底搞懂浏覽器的緩存機制

無論在工作中還是平時的學習面試過程中,HTTP緩存幾乎都是我們繞不開的話題,面對這些常見的知識點,我們不應該選擇逃避,而是勇于面對,去搞懂它們。

為什麼需要緩存?

在任何一個前端項目中,通路伺服器擷取資料都是很常見的事情,如果相同的資料被重複請求了不止一次,那麼多餘的請求必然會浪費網絡帶寬,以及延遲浏覽器渲染所要處理的内容,進而影響使用者的使用體驗。如果使用者使用的是按量計費的方式通路網絡,多餘的請求還會隐形的增加使用者的網絡流量資費。是以考慮使用緩存技術對已經擷取的資源進行重用,是一種提升網站性能與使用者體驗的有效政策。

緩存的原理

緩存的原理是在首次請求後儲存一份請求資源的響應副本,當使用者再次發起相同請求後,如果判斷緩存命中則攔截請求,将之前存儲的響應副本傳回給使用者,進而避免了重新向伺服器發起資源請求。

HTTP緩存

HTTP緩存應該算是前端開發中最常接觸的緩存之一,它又可以細分為強制緩存和協商緩存,二者最大的差別在于判斷緩存命中時,浏覽器是否需要向伺服器端進行詢問以協商緩存的相關資訊,進而判斷是否需要就響應内容進行重新請求,下面讓我們來看看HTTP緩存的具體機制及緩存的決策政策。

強制緩存

對于強制緩存而言,如果浏覽器判斷所請求的目标資源有效命中,則直接從強制緩存中傳回請求響應,無須與伺服器進行任何通信。

其中與強制緩存相關的兩個字段是expires和cache-control,expires是在HTTP1.0協定中聲明的用來控制緩存失效日期時間戳的字段,它由伺服器端指定後通過響應頭告知浏覽器,浏覽器在接收到帶有該字段的響應體後進行緩存。

若之後浏覽器再次發起相同的資源請求,便會對比expires與本地目前的時間戳,如果目前請求的本地時間戳小于expires的值,則說明浏覽器緩存的響應還未過期,可以直接使用而無須向伺服器端再次發起請求。隻有當本地時間戳大于expires值,發生緩存過期時,才允許重新向伺服器發起請求。

從上述強制緩存的是否過期的判斷機制中不難看出,這個方式存在一個很大的漏洞,即對本地時間戳過分依賴,如果用戶端本地的時間與伺服器端的時間不同步,或者對用戶端的時間進行主動修改,那麼對于緩存過期的判斷可能就無法和預期相符。

為了解決expires判斷的局限性,從HTTP1.1協定開始新增了cache-control字段來對expires的功能進行拓展和完善。從上述代碼中可見cache-control設定了maxage=31536000的屬性值來控制響應資源的有效期,它是一個以秒為機關的時間長度,表示該資源在被請求到後的31536000秒内有效,如此便可避免伺服器端和用戶端時間戳不同步而造成的問題。

注意:如果Cache-Control的max-age和expires同時存在,則以max-age為準。

Cache-Control的其他參數

  • no-cache
設定no-cache并非不适用緩存,而是表示強制進行協商緩存,即對于每次發起的請求都不會再去判斷強制緩存是否過期,而是直接與伺服器寫撒謊給你來驗證緩存的有效性,若緩存未過期,則會使用本地緩存。
  • no-store
設定no-store則表示禁止使用任何緩存,用戶端的每次請求都需要伺服器端給予全新的響應。no-cache與no-store是兩個互斥的屬性值,不能同時設定。
  • public
若資源響應頭中的cache-control字段設定了public屬性值,則表示響應資源既可以被浏覽器緩存,又可以被代理伺服器緩存。
  • private
private則限制了響應資源隻能被浏覽器緩存,如果沒有顯示指定則預設值是private。
  • max-age
表示伺服器端告知用戶端浏覽器響應資源的過期時長。
  • s-maxage

對于大型架構的項目通常會涉及使用各種代理伺服器的情況,這就需要考慮緩存在代理伺服器上的有效性問題,這邊是s-maxage存在的意義,它表示緩存在代理伺服器中的過期時長,且僅當設定了public屬性值時才是有效的。

由此可見,cache-control能夠作為expires的完全替代方案,并且擁有其所不具備的一些緩存控制特性,在項目實踐中使用它就足夠了,目前expires還存在的唯一理由就是向下相容。

協商緩存

顧名思義,協商緩存就是在使用本地緩存之前,需要向伺服器發起一次GET請求,與之協商目前浏覽器儲存的本地緩存是否已經過期。通常是采用所請求資源的最近一次的修改時間戳來判斷的。
  • 執行個體

假設用戶端需要向伺服器請求一個manifest.js的JS檔案,為了讓該資源被再次請求時能夠通過協商緩存的機制使用本地緩存,那麼首次傳回該圖檔資源的響應頭中應包含一個名為last-modified的字段,該字段的屬性值為該JS檔案最近一次修改的時間戳。

當我們重新整理網頁時,由于該JS檔案使用的是協商緩存,用戶端浏覽器無法确定本地緩存是否過期,是以需要向伺服器發送一次GET請求,進行緩存有效性的協商,此次GET請求的請求頭中需要包含一個ifmodified-since字段,其值正是上次響應頭中last-modified的字段值。

當伺服器收到該請求後便會對比請求資源目前的修改時間戳與if-modified-since字段的值,如果二者相同則說明緩存未過期,可繼續使用本地緩存,否則伺服器重新傳回全新的檔案資源。

基于Last-Modified的協商緩存(伺服器端代碼)

const data = fs.readFileSync('./imgs/CSS.png');
 const { mtime } = fs.statSync('./imgs/CSS.png');
 const ifModifiedSince = req.headers['if-modified-since'];
 if (ifModifiedSince === mtime.toUTCString()) {
    res.statusCode = 304;
    res.end();
    return
 }
 res.setHeader('last-modified',mtime.toUTCString())
 res.setHeader('Cache-Control','no-cache');
 res.end(data);
複制代碼      

Last-Modified協商緩存流程

用戶端第一次請求目标資源的時,伺服器傳回的響應标頭包含last-modified和該資源的最後一次修改的時間戳,以及cache-control:no-cache,當用戶端再次請求該資源的時候,會攜帶一個ifmodifiedsince字段,如果這個字段對應的時間與目标資源的時間戳進行對比,如果沒有變化則傳回一個304狀态碼。

需要注意的是:協商緩存判斷緩存有效的響應狀态碼是304,但是如果是強制緩存判斷有效的話,響應狀态碼是200。

last-modified的不足

  1. last-modified是根據請求資源的最後修改時間戳進行判斷的,雖然請求的檔案資源進行了編輯,但是内容并沒有發生任何變化,時間戳也會更新,進而導緻協商緩存時關于有效性的判斷驗證為失效,需要重新進行完整的資源請求。這無疑會造成網絡帶寬資源的浪費,以及延長使用者擷取到目标資源的時間。
  2. 辨別檔案資源修改的時間戳機關是秒,如果檔案修改的速度非常快,假設在幾百毫秒内完成,那麼通過時間戳的方式來驗證緩存的有效性,是無法識别出該次檔案資源的更新的。
其實造成上述兩種缺陷的原因相同,就是伺服器無法根據資源修改的時間戳識别出真正的更新,進而導緻重新發起了請求,該重新請求卻使用了緩存的Bug場景。

基于Etag的協商緩存(服務端代碼)

一篇文章,徹底搞懂浏覽器的緩存機制

為了彌補通過時間戳判斷的不足,從HTTP1.1規範開始新增了一個Etag的頭資訊,即實體标簽。 其内容主要是伺服器為不同的資源進行哈希計算所生成的一個字元串,該字元串類似于檔案指紋,隻要檔案内容編碼存在差異,對應的Etag對檔案資源進行更精準的變化感覺。

Etag協商緩存的流程

  1. 首先,服務端将要傳回給用戶端的資料通過etag子產品進行哈希計算生成一個字元串,這個字元串類似于檔案指紋。
  2. 檢測用戶端的請求标頭中的ifNoneMatch字段的值和第一步計算的值是否一緻,一緻則傳回304。
  3. 如果不一緻則傳回etag标頭和Cache-Control:no-cache。

Etag的不足

不像強制緩存中cache-control可以完全替代expires的功能,在協商緩存中,Etag并非last-modified的替代方案而是一種補充方案,因為依舊存在一些弊端。
  1. 伺服器對于生成檔案資源的Etag需要付出額外的計算開銷,如果資源的尺寸比較大,數量較多且修改頻繁,那麼生成的Etag的過程就會影響伺服器的性能。
  2. Etag字段值的生成分為強驗證和弱驗證,強驗證根據資源内容進行生成,能夠保證每個位元組都相同,弱驗證則根據資源的部分屬性來生成,生成速度快但無法確定每個位元組都相同,并且在伺服器叢集場景下,也會因為準确不夠而降低協商緩存有效性的成功率,是以恰當的方式是根據具體的資源使用場景選擇恰當的緩存校驗方式。

緩存決策及其注意事項

緩存決策

假設在不考慮用戶端緩存容量與伺服器算力的理想情況下,我們當然希望用戶端浏覽器上的緩存觸發率盡可能高,留存時間盡可能長,同時還要Etag實作當資源更新時進行高效的重新驗證。但實際情況往往是容量與算力都有限,是以就需要制定合适的緩存政策,來利用有限的資源達到最優的性能效果,明确能力的邊界,力求在邊界内做到最好。

緩存決策樹

在面對一個具體的緩存需求時,我們可以參照如下的緩存決策樹來逐漸确定對一個資源具體的緩存政策。
  • 是否使用緩存
    • 否:no-store
    • 是:
      • 是否進行協商緩存
        • 是:no-cache
          • 是否會被代理伺服器緩存
            • 是:public
            • 否:private
              • 配置強制緩存過期時間
                • 配置協商緩存的Etag或last-modified。

CDN緩存

什麼是CDN?

CDN全稱是内容分發網絡,它是建構在現有網絡基礎上的虛拟智能網絡,依靠部署在各地的邊緣伺服器,通過中心平台的負載均衡、排程及内容分發等功能子產品,使使用者在請求所需通路的内容時能夠就近擷取,以此來降低網絡擁塞,提高資源對使用者的響應速度。

不使用CDN的通信流程

  1. 向傳統的DNS伺服器請求域名解析。
  2. DNS伺服器傳回域名對應的伺服器IP。
  3. 根據伺服器IP請求伺服器内容。
  4. 伺服器傳回響應資源。

使用CDN的通信流程

  1. 用戶端向傳統的DNS伺服器請求域名解析。
  2. 傳統的DNS伺服器将域名解析權交給了CNAME指向的專用DNS伺服器,是以對使用者輸入域名的解析最終是在CDN專用的DNS伺服器上完成的。
  3. CDN專用的DNS伺服器将CDN負載均衡器的IP發給用戶端。
  4. 浏覽器會重新向CDN負載均衡器發起請求,經過對使用者IP位址的距離、所請求資源内容的位置等的綜合計算,傳回給使用者确定的緩存伺服器IP位址。
  5. 浏覽器最後對緩存伺服器進行請求資源。

靜态資源适合使用CDN

靜态資源指的是不需要網站業務伺服器參與計算即可得到的資源,包括第三方庫的JavaScript腳本檔案、樣式表檔案以及圖檔等,這些檔案的特點是通路頻率高、承載流量大、但更新頻次低,且不與業務有太多耦合。

如果是動态資源檔案,比如依賴伺服器端渲染得到的HTML頁面,它需要借助伺服器端的資料進行計算才能得到,是以這樣的資源不适合存放在CDN緩存伺服器上。

CDN的性能優化

下面僅介紹一個CDN優化點:域名設定。

在淘寶的首頁上,主站請求的域名為

www.taobao.com ,而靜态資源請求CDN伺服器的域名有g.alicdn.com和img.alicdn.com兩種,這樣做的原因有以下兩點:
  1. 避免對靜态資源的請求攜帶不必要的cookie資訊。
  2. 考慮浏覽器對同一域名下并發請求的限制。

面試常見問題

問題1:強緩存涉及到哪些請求頭?

答:涉及到expires和cache-control兩個字段,expires是HTTP1.0協定中的,cache-control是HTTP/1.1協定的。

問題2:為什麼現在不用expries用cache control?

答:因為基于expires的強制緩存對本地時間戳過于依賴,如果用戶端本地的時間與伺服器端的時間不同步,那麼對緩存過期的判斷可能就會出錯。cache-control通過maxage=xxx秒的形式來控制響應資源的有效期,如此可以避免伺服器端和用戶端時間戳不同步的問題。

問題3:強緩存public private no-store no-catch差別?(Cache-Control有哪些屬性?分别表示什麼意思?)

public:表示響應資源既可以被用戶端緩存也可以被代理伺服器緩存。 private:表示響應資源隻能被浏覽器緩存,如果沒有顯式指定則預設是private no-store:表示禁止使用任何緩存,每次請求都需要伺服器給與全新的響應。 no-cache:表示使用協商緩存。每次請求不再去判斷強制緩存是否過期,而是直接向伺服器發送請求來驗證緩存的有效性。 max-age:表示伺服器端告知用戶端浏覽器響應資源的過期時長。 s-maxage:表示緩存在代理伺服器中的過期時長,且僅當設定了public屬性值時才是有效的。

問題4:協商緩存的校驗是在用戶端還是伺服器端?協商緩存怎麼驗證是否命中?

答:伺服器端,伺服器端會對比檔案最後的修改時間和用戶端請求攜帶的時間是否一緻,一緻則判斷命中緩存。協商緩存存在兩種形式,一種是基于last-modified,用戶端第一次請求目标資源的時候,伺服器傳回的響應标頭中包含last-modified和該資源的最後一次修改的時間戳,以及cache-control:no-cache,當用戶端再次請求該資源的時候,會攜帶一個ifmodifiedsince字段,如果這個字段對應的時間和目标資源的時間戳進行對比,沒有變化則傳回304狀态碼。另一種是基于Etag的協商緩存,手下服務端将要傳回給用戶端的資料通過etag子產品進行哈希計算生成一個字元串,這個字元串類似于檔案指紋,檢測用戶端的請求标頭中的ifNoneMatch字段的值和第一步計算的值是否一緻,一緻則傳回304,不一緻則傳回最新的資料以及etag标頭和Cache-Control:no-cache。

問題5:協商緩存出于什麼原因有Last-Modified,Etag?

答:之是以有last-modified還有etag,是因為這二者均有自己的不足,last-modified是根據請求資源的最後修改時間戳來進行判斷的,有可能隻是對檔案名進行了編輯,但是檔案内容并未修改,這樣時間戳也會更新,進而導緻協商緩存判斷失效,請求了已經存在的完整資源,這對網絡帶寬是一種浪費,也有可能是檔案修改的速度是毫秒級别的,但是last-modified的機關是秒,可能無法識别出資源的修改。etag并非last-modified的完全替代方案,隻能是一種補充方案,etag存在的問題是,伺服器需要對檔案資源進行etag計算,需要付出額外的計算開銷,如果資源的尺寸比較大,生成Etag的過程可能會影響伺服器的性能,是以這也就是為什麼協商緩存既有last-modified又有etag的原因了。

問題6:協商緩存和強緩存的差別?

相同點

都是從用戶端緩存中讀取資源。

不同點

  1. 如果浏覽器命中的是強緩存,則不需要給伺服器發請求,而協商緩存最終由伺服器來決定是否使用緩存,即用戶端與伺服器之間存在一次通信。
  2. 在chrome中命中緩存,傳回的狀态碼是200,而如果是協商緩存,傳回的是狀态碼304。

問題7:expires 和 cache-control 哪個優先級高? 不緩存怎麼設定?

答:expires是HTTP/1.0的産物,Cache-Control則是HTTP/1.1的産物,二者如果同時存在的話,Cache-Control優先級比Expires高。不緩存則是通過Cache-Control:no-store設定。

問題8:LastModified 對應有個請求頭是什麼?

last-modified-since.

問題9:緩存的優先級順序?

答:Cache-Control > Expires > Etag > Last-Modified。

繼續閱讀