HTTP 緩存

rn丶 2021-08-15 17:02:26 阅读数:152

本文一共[544]字,预计阅读时长:1分钟~
http

前言

在任何一個前端項目中,訪問服務器獲取數據都是很常見的事情,但是如果相同的數據被重複請求,那麼多餘的請求次數必然會浪費網絡帶寬,以及延遲瀏覽器渲染所要處理的內容,從而影響用戶的使用體驗。如果用戶使用的是按量計費的方式訪問網絡,那麼多餘的請求還會隱性地增加用戶的網絡流量資費。因此考慮使用緩存技術對已獲取的資源進行重用,是一種提昇網站性能與用戶體驗的有效策略。

緩存的原理

在首次請求後保存一份請求資源的響應副本,當用戶再次發起相同請求後,如果判斷緩存命中則攔截請求,將之前存儲的響應副本返回給用戶,從而避免重新向服務器發起資源請求。

緩存的技術種類有很多,比如代理緩存瀏覽器緩存網關緩存負載均衡器內容分發網絡等,它們大致可以分為兩類:

  • 共享緩存 - 緩存內容可被多個用戶使用,如公司內部的Web代理。
  • 私有緩存 - 只能單獨被用戶使用的緩存,如瀏覽器緩存。

HTTP 緩存 算是前端開發中最常接觸的緩存機制之一,它又可細分為 强制緩存協商緩存,二者最大的區別在於判斷緩存命中時,瀏覽器是否需要向服務器端進行詢問以協商緩存的相關信息,進而判斷是否需要就響應內容進行重新請求。 下面就來具體看HTTP緩存的具體機制及緩存的决策策略。

强制緩存

對於强制緩存而言,如果瀏覽器判斷所請求的目標資源有效命中(還沒有過期),則可直接從强制緩存中返回請求響應,無須與服務器進行任何通信。

簡單地說就是,不會向服務器發送請求,直接從緩存中讀取資源。

强制緩存原理

强制緩存就是向瀏覽器緩存查找該請求結果,並根據該結果的緩存規則來决定是否使用該緩存結果的過程,强制緩存的情况主要有三種,如下:

  • 第一種情况: 第一次請求,不存在緩存結果和緩存標識,直接向服務器發送請求。

  • 第二種情况: 存在緩存標識和緩存結果,但是已經失效過期,强制緩存失效,則使用協商緩存(後面會介紹)

  • 第三種情况: 存在該緩存結果和緩存標識,且該結果尚未失效,强制緩存生效,直接返回該結果。

强制緩存的緩存規則

當瀏覽器向服務器發起請求時,服務器會將緩存規則放入HTTP響應報文的HTTP頭中和請求結果一起返回給瀏覽器,控制强制緩存的字段分別是ExpiresCache-Control其中Cache-Control優先級比Expires

在介紹 强制緩存 判斷是否命中之前,我們首先來看一段響應頭的部分信息:

 access-control-allow-origin: *
 age: 734978
 content-length: 40830
 content-type: image/jpeg
 cache-control: max-age=31536000
 expires: Web, 14 Fed 2021 12:23:42 GMT
複制代碼

其中和强制緩存相關的兩個字段是 expirescache-control

Expires

  • expires 是在 HTTP1.0協議 中聲明的用來控制緩存失效日期時間戳的字段。它在服務器端設置後通過響應頭告知瀏覽器,瀏覽器在接收到帶有該字段的響應體後進行緩存。
  • 有效值:GMT 格式
  • Expires = max-age + 請求時間

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

 resp.setHeader("Expires", new Date("2021-08-13 22:50:24").toUTCString());
複制代碼

Expires 的缺陷:

上述的 强制緩存是否過期的判斷機制 存在一個很大的漏洞,即對本地時間戳過分依賴,如果客戶端本地的時間與服務器端的時間不同步,或者對客戶端時間進行主動修改,那麼對於緩存過期的判斷可能就無法和預期相符。

Cache-control

為了解决 Expires 判斷的局限性,從 HTTP1.1協議 開始新增了 Cache-control 字段來對 Expires 的功能進行擴展和完善。

max-age

從上述代碼中可見 Cache-control 設置了 max-age=31536000 的屬性值來控制響應資源的有效期,它是一個以 為單比特的時間長度,錶示該資源在被請求後的 31536000秒 內有效。如此便可避免服務器端和客戶端時間戳不同步而造成的問題。除此之外,Cache-control 還可配置一些其他屬性值來更准確地控制緩存,下面來具體介紹:

no-cache 和 no-store

設置 no-cache 並非像字面上的意思不使用緩存,其錶示為强制進行 協商緩存(後面會說),即對於發起的請求不再去判斷强制緩存是否過期,而是直接與服務器協商來驗證緩存的有效性,若緩存未過期,則會使用本地緩存。

設置 no-store 則錶示禁止使用任何緩存策略,客戶端的每次請求都需要服務端給予全新的響應結果。

no-cacheno-store 是兩個互斥的屬性值,不能同時設置。

private 和 public

privatepublic 也是 Cache-control 的一組互斥屬性值,它們用以明確響應資源是否可被代理服務器進行緩存。

  • private - 錶示響應資源既可以被瀏覽器緩存,又可以被代理服務器緩存。
  • public - Cache-control的默認取值。錶示響應資源只能被瀏覽器緩存。

對於應用程序中不會改變的文件,通常可以在發送響應頭前添加積極緩存(public)。例如應用程序中的靜態文件,例如圖像、CSS文件和JS文件。

 Cache-control: public, max-age=600
複制代碼

max-age 和 s-maxage

max-age 屬性值比 s-maxage 更常用,它(max-age)錶示服務器端告知客戶端瀏覽器響應資源的過期時長,在一般項目的使用場景中基本够用。 但對於大型架構的項目通常會涉及使用各種代理服務器的情况,這就需要考慮緩存在代理服務器上的有效性問題。這便是 s-maxage 存在的意義,它錶示緩存在代理服務器中的過期時長,且僅當設置了 public 屬性值才有效。

總結

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

協商緩存

協商緩存就是在使用本地緩存之前,需要向服務器端發起一次 GET 請求,與之協商當前瀏覽器保存的本地緩存是否已經過期

Last-Modified

通常是采用所請求資源最近一次的修改時間戳(獲取文件的 mtime 時間)來判斷的,為了便於理解,下面來看一個例子:假設客戶端瀏覽器需要向服務器請求一個 main.js 的 JS 文件資源,為了讓該資源被再次請求時能通過協商緩存的機制使用本地緩存,那麼首次返回該圖片資源的響應頭中應包含一個 Last-Modified 字段,該字段屬性值為該 JS 文件資源最近一次修改的時間戳。簡略截取 請求頭 與 響應頭 的關鍵信息:

 // 第一次請求的請求頭
 Request URL: http://localhost:3000/image.jpg
 Request Method: GET
 // 第一次請求的響應頭
 last-modified: Thu, 29 Apr 2021 03:09:28 GMT
 cache-control: no-cache
複制代碼

當我們刷新網頁時,由於該 JS 文件使用的是協商緩存(no-cache),客戶端瀏覽器無法確定本地緩存是否過期,所以需要向服務器發送一次 GET 請求,進行緩存有效性的協商,此次 GET 請求的請求頭中需要包含一個 If-Modified-Since 字段,其值就是上次響應頭中 Last-Modified 的字段值。

當服務器收到該請求後便會對比請求資源當前的修改時間戳與 If-Modified-Since 字段的值,如果二者相同則說明緩存未過期,告知瀏覽器可繼續使用本地緩存,否則服務器重新返回全新的文件資源,簡略截取 請求頭 與 響應頭 的關鍵信息:

 // 再次請求的請求頭
 Request URL: http://localhost:3000/image.jpg
 Request Method: GET
 If-Modified-Since: Thu, 29 Apr 2021 03:09:28 GMT
 ​
 // 協商緩存有效的響應頭
 Status Code: 304 Not Modified
複制代碼

*!注意:*協商緩存 判斷緩存是否有效的響應狀態碼是 304,即緩存還有效就重定向到本地緩存上。這和强制緩存有所不同,强制緩存是若有效,則再次請求的響應狀態碼是 200

Last-Modified 的缺陷

通過 Last-Modified 所實現的協商緩存能够滿足大部分的使用場景,但也存在兩個比較明顯的缺陷:

  • 首先它只是根據資源最後的修改時間戳進行判斷的,雖然請求的文件資源進行了編輯,但內容並沒有發生任何變化(如修改文件名稱,隨後有修改回來 【a.jpg -> b.jpg -> a.jpg】),時間戳也會更新,從而導致協商緩存時關於有效性的判斷驗證為失效,需要重新進行完整的資源請求。這無疑會造成網絡帶寬資源的浪費,以及延長用戶獲取到目標資源的時間。
  • 其次標識文件資源修改的時間戳單比特是秒,如果文件修改的速度非常快,假設在幾百毫秒內完成,那麼上述通過時間戳的方式來驗證緩存的有效性,是無法識別出該次文件資源的更新的。(如1秒內多次修改是無法捕捉到的

其實造成上述兩種缺陷的原因相同,就是服務器無法僅依據資源修改的時間戳來識別出真正的更新,進而導致重新發起了請求,該重新請求卻使用了緩存的 Bug 場景。

ETag

為了彌補通過時間戳判斷的不足,從 HTTP 1.1協議 開始新增了一個 ETag 的頭信息,即實體標簽(Entity Tag)。

其內容主要是服務器根據對不同資源進行哈希運算所生成的字符串,該字符串類似於文件指紋,只要文件內容編碼存在差异,對應的 ETag 值就會不同。因此可以使用 ETag 對文件資源進行更精確的變化感知。

下面使用 ETag 進行協商緩存圖片資源為例,首次請求後的部分響應頭關鍵信息:

 Content-Type: image/jpeg
 ETag: "xxx"
 Last-Modified: Fri, 12 Jul 2021 18:30:00 GMT
 Content-Length: 9887
複制代碼

上面響應頭中同時包含了 last-modified 文件修改時間戳和 ETag 實體標簽兩種協商緩存的有效性校驗字段,因為 ETagLast-Modified 具有更准確的文件資源變化感知,所以它的優先級也更高,二者同時存在時會以 ETag 為准。

再次對該圖片資源發起請求時,就會將之前首次請求的響應頭的 ETag 的字段值作為此次請求頭中 If-None-Match 字段,提供給服務器進行緩存有效性驗證。

請求頭的關鍵字段信息:

 // 再次請求的請求頭:
 If-Modified-Since: Fri, 12 Jul 2021 18:30:00 GMT
 If-None-Match: "xxx"
複制代碼

若驗證緩存有效,則返回 304 狀態碼響應重定向到本地緩存。

ETag 的缺陷:

不像强制緩存中的 cache-control 可以完全替代 expires 的功能,在協商緩存中,ETag 並非 Last-Modified 的替代方案而是一種補充方案,因為它依舊存在一些弊端。

  • 一方面服務器對於生成文件資源的 ETag 需要付出額外的計算開銷,如果資源的尺寸較大,數量較多且修改比較頻繁,那麼生成 ETag 的過程就會很影響服務器的性能。

  • 另一方面 ETag 字段值的生成還分為 强驗證弱驗證

    • 强驗證:根據資源內容進行生成,能够保證每個字節都相同;
    • 弱驗證:根據資源的部分屬性值來生成,生成速度快但無法確保每個字節都相同,並且在服務器集群場景下,也會因為不够准確而降低協商緩存有效性驗證的成功率。

網站鏈接

版权声明:本文为[rn丶]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815170211220K.html