前端網絡編程之緩存策略

前言

爲何要寫這篇文章?
故事背景發生在 2019 年的某一天,國內某大型開源 CDN 網站服務器奔潰以後。公司的 H5 產品,引用了他們的 CDN 資源,當天沒法訪問,後來咱們緊急發佈了熱修復,用阿里雲 CDN 資源替換了線上的連接地址。css

發佈熱補丁以後,咱們指望的結果固然是用戶從新打開微信公衆號訪問 H5 就能正常加載最新的資源,正常訪問。html

可是,最後的問題是,iOS 系統的微信公衆號可以正常訪問,安卓卻不行,安卓加載的仍是緩存中的靜態資源文件。前端

這裏面固然是由於微信安卓的瀏覽器內核對緩存作了特殊的處理(坑),可是怎麼從根本上解決緩存問題?nginx

關於 HTTP 緩存的文章網上不少,因此我這裏主要作簡單總結和問題探討。算法

問題:跨域

  1. HTTP 緩存策略是什麼樣的原理?
  2. 緩存響應的狀態碼 200 和 304 狀態碼有什麼區別?
  3. 如何高效的利用緩存?
  4. 假如想利用 HTTP 請求統計用戶訪問量,有緩存不就沒用了?

先給結論:瀏覽器

  1. 緩存策略分爲強制緩存和協商緩存
  2. 緩存位置分爲 CacheStorage、內存緩存、硬盤緩存
  3. 200 表明從本地緩存中獲取,304 表明和服務器進行過一次通訊後從瀏覽器緩存中獲取
  4. 儘量將靜態資源的緩存週期變長,index.html 不該該被緩存,利用 GET 請求進行緩存
  5. 利用 1*1 px 透明圖片埋點和 referer 來統計用戶訪問量,該圖片路徑不緩存

1、HTTP 緩存

緩存是一種保存資源副本並在下次請求時直接使用該副本的技術緩存

瀏覽器緩存通常只有 GET 請求才有效。性能優化

緩存的種類有不少,其大體可歸爲兩類:私有與共享緩存。共享緩存存儲的響應可以被多個用戶使用。私有緩存只能用於單獨用戶。bash

"public" 指令表示該響應能夠被任何中間人(譯者注:好比中間代理、CDN等)緩存,隱含的意思是,其餘用戶可能也能分享這個資源的緩存

private 是指資源應該被緩存,可是隻能被客戶端的瀏覽器緩存.

下面是和 HTTP 緩存有關的 HTTP 消息頭:

HTTP Header 出如今 應用在
Cache-Control 請求、響應 強制和協商緩存
Expires 響應 強制緩存
Last-Modified 響應 協商緩存
If-Modified-Since 請求 協商緩存
ETag 響應 協商緩存
If-None-Match 請求 協商緩存

2、緩存位置

文件的緩存位置,目前廣泛使用的有三種,其中 cacheStorage 是在 ServiceWorker 中應用中產生的。

緩存位置 資源
from ServiceWorker(cacheStorage) 經過 ServiceWorker 註冊安裝
from memory cache 腳本、字體、圖片
from disk cache 非腳本:css、svg

緩存放在內存仍是硬盤中,也會和文件大小有關係。

3、強制緩存

強制緩存是經過 Expires 和 Cache Control 的 max-age 來判斷緩存是否過時的策略,此時不會向服務器發起請求。

若是緩存沒有過時,將會直接從瀏覽器緩存中獲取資源。

Expires 的值一般是一個絕對時間,存在的問題當是客戶端和服務器時間不一致或者被修改的狀況下就會失效。而 max-age 的值是相對時間,根據瀏覽器緩存根據服務器返回的 Date 和 Max-Age 判斷緩存是否過時。

打個比方:max-age 至關於說「保質期六個月」,而 Expires 是說「在此日期以前」飲用。

max-age 和 Expires 設置的緩存過時時間最多爲一年(365天),若是多於這個值則瀏覽器有可能會忽略

當 Expires 和 Max-Age 同時存在時,會忽略 Expires 指令。

做爲請求首部時,cache-directive 的可選值

字段名稱 說明
no-cache 告知(代理)服務器不直接使用緩存,要求向原服務器發起請求
no-store 全部內容都不會被保存到緩存或 Internet 臨時文件中,直接下載文件
max-age=delta-seconds 告知服務器客戶端但願接收一個存在時間(Age)不大於 delta-seconds 秒的資源, 爲 0 表示直接進行協商緩存
max-stale[=delta-seconds] 告知(代理)服務器端願意接收一個超過緩存時間的資源,若定義delta-seconds則爲delta-seconds秒,若沒有則爲任意超出時間
min-fresh=delta-seconds 全部內容都不會被保存到緩存或 Internet臨時文件中
no-transform 告知(代理)服務器客戶端但願獲取實體數據沒有被轉換(好比壓縮)過的資源
only-if-cached 告知(代理)服務器客戶端但願獲取緩存的內容(如有), 而不用向原服務器發去請求
cache-extension 自定義擴展值,若服務器不識別該值將被忽略掉

Cache-Control 在響應中的字段:

字段名稱 說明
public 指示響應能夠被任何緩存所緩存,即便一般它只是非可緩存或可緩存到一個非共享緩存內
private 指示響應信息的所有或者部分用於單個用戶,而不能用一個共享緩存來緩存
no-cache 能夠緩存,可是隻有在跟 WEB 服務器驗證了其有效後,才能返回給客戶端(直接進行協商緩存)
no-store 全部內容不會被保存到緩存或 Internet 臨時文件中,指令的目的是防止無意發佈或是保留了敏感信息(例如,備份)
no-transform 告知客戶端緩存文件時不得對實體數據作任何改變。它對__轉換某個實體體的媒體類型__頗有用,例如,一個非透明的代理把圖像轉換格式,以節省緩存空間,或是減小慢速連接中的通訊量
only-if-cached 在某些狀況下,如網絡鏈接很是差時,客戶端可能須要一個緩存,只返回目前已存儲的那些響應,而不是從新加載,或與源服務器從新驗證。要作到這一點,客戶端能夠在一個請求中包含該指令。
must-revalidate 當前資源必定是向原服務器發去驗證請求的,若請求失敗會返回__504__(而非代理服務器上的緩存)
proxy-revalidate 與 must-revalidate相似,可是僅能應用於共享緩存(如代理)
max-age=delta-seconds 告知客戶端該資源在delta-seconds秒內是新鮮的,除非還包含 max-stale 指令,不然客戶端不指望接收一個陳舊的響應。
s-maxage=delta-seconds 同 max-age,但僅應用於共享緩存(如代理)
cache-extension 自定義擴展值,若服務器不識別該值將被忽略

4、協商緩存

Last-Modified 和 If-Modified-Since 是用最後修改日期時間判斷一個緩存的資源是否有效。

  1. 服務器第一次請求會發送 Last-Modified 響應頭
  2. 瀏覽器下次協商請求時,會將 If-Modified-Since 頭加上 緩存響應中的 Last-Modified 值合在一塊兒發送給服務器,
  3. 服務器判斷沒過時,就會返回 304 狀態碼,若是過時會直接返回資源
  4. 若是狀態碼是 304, 瀏覽器就從緩存中獲取資源

Last-Modified 存在的問題多是,將打包的前端資源文件夾進行覆蓋式部署時,部分文件內容並無發生變化,可是修改時間卻被更新了,此時瀏覽器會下載資源,形成了沒必要要的時延和帶寬消耗。

Etag 和 If-None-Match 則是用內容摘要做爲斷定的依據。內容摘要是指根據一個資源的內容產生一串比較短的數字,當內容變化時,產生的數字串也會改變。內容摘要的算法有不少種,較常見的是SHA-1哈希算法、CRC32等。

Etag 的運行流程和 Last-Modified 同樣:

  1. 第一次請求時,服務器返回文件的 etag
  2. 後面的請求,瀏覽器將 If-None-Match 頭帶上緩存響應中的 etag 值合在一塊兒,發送給服務器
  3. 服務器判斷沒過時,就會返回 304 狀態碼,若是過時就會直接返回資源。
  4. 若是狀態碼是 304, 瀏覽器就從緩存中獲取資源

Etag 的優先級會比 Last-Modified 更高, Etag 就是爲了解決 Last-Modified 文件更新時間變化,但文件內容沒變的問題,另外 Etag 的缺點在於會佔用比 Last-Modified 更高的服務器 CPU 消耗。

5、200 和 304

由以上的知識,咱們瞭解到:

強制緩存返回的 HTTP 響應狀態碼是 200,而協商緩存返回的響應狀態碼是 304。

大多數狀況是這樣沒錯,可是在部分瀏覽器中(好比谷歌),協商緩存也是會返回 200 的,這是瀏覽器的算法使然,瀏覽器判斷,越長時間沒有更新的文件,會直接從瀏覽器緩存中獲取資源,此時狀態碼是 200.

下面是網上常見強制緩存和協商緩存的流程圖,我也畫了一個:

6、緩存更新

這個問題主要在產品部署發佈迭代版本的時候會遇到。

若是服務器沒有配置 Last-Modified 和 etag, 當強制緩存尚未失效的時候,如何更新文件版本呢?

目前廣泛的作法有兩種,不緩存 index.html,在 js,css,字體,圖片的連接地址後拼接版本參數:好比

cdn.xienanbo.com/logo.png?v=…

或者將資源文件的內容 HASH 短碼添加在文件名中:

cdn.xienanbo.com/dist.a87e5a…

7、談談微信瀏覽器的坑

微信安卓版內置瀏覽器符合通常瀏覽器的緩存策略。可是 iOS 內置瀏覽器會將 index.html 文件強制緩存,儘管你在 index.html 文件的 head 標籤上添加了以下代碼:

<meta http-equiv="Cache-Control" content="no-cache, no-store" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="expires" content="0" />
複製代碼

上面 Pragma 是 HTTP1.0 的消息頭,基本沒用,僅有 IE 才能識別這段 meta 標籤含義。 解決方案就是從服務器層面對 index.html 文件進行緩存控制,好比 Nginx:

location / {
  root  /var/www/;
  index index.html index.htm;
  try_files $uri $uri/ /index.html;
  
  #### kill cache
  add_header Last-Modified $date_gmt;
  add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
  if_modified_since off;
  expires off;
  etag off;
}

location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|js)$ {
  root  /mnt/dat1/test/tes-app;
  access_log off;
  expires 30d;
}
複製代碼

大公司通常採用 index.html 和 js、css、字體、圖片資源利用 CDN 分開部署,不在同一臺服務器上,因此只須要對 index.html 設置不緩存便可。

順便提一下,若是是字體文件部署在 CDN 上,會存在跨域問題(CSS、JS、圖片不會受到同源政策的限制),開啓 CORS 便可:

access-control-allow-origin: *
複製代碼

總結

瀏覽器緩存是前端性能優化中關鍵的一步,利用 GET 請求緩存文件。

瀏覽器緩存分爲強制緩存和協商緩存,強制緩存中 Cache-Control 比 Expires 的優先級更高。協商緩存中,If-None-Match 比 If-Modified-Since 的優先級更高。

強制緩存返回狀態碼 200,協商緩存狀態 304。

前端打包應該利用打包工具對文件名添加 hash 短碼,如需校驗身份信息應該利用協商緩存和私有緩存。

參考連接:

@Starbucks 2019/04/13

相關文章
相關標籤/搜索