瀏覽器緩存機制及實現方式

什麼是緩存

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

這裏的web瀏覽器緩存主要指http緩存。css

緩存的做用

  • 減小網絡延遲,加快頁面打開速度
  • 減小網絡帶寬消耗
  • 下降服務器壓力

緩存分類

按使用權限分

  • 私有緩存,私有緩存只能用於單獨用戶
  • 共享緩存,共享緩存存儲的響應可以被多個用戶使用,例如,ISP 或你所在的公司可能會架設一個 web 代理來做爲本地網絡基礎的一部分提供給用戶。這樣熱門的資源就會被重複使用,減小網絡擁堵與延遲。

按存儲位置分

瀏覽器本地緩存

內存緩存(from memory cache)

內存緩存具備兩個特色,分別是快速讀取和時效性:快速讀取:內存緩存會將編譯解析後的文件,直接存入該進程的內存中,佔據該進程必定的內存資源,以方便下次運行使用時的快速讀取。時效性:一旦該進程關閉,則該進程的內存則會清空。html

硬盤緩存(from disk cache)

硬盤緩存則是直接將緩存寫入硬盤文件中,讀取緩存須要對該緩存存放的硬盤文件進行I/O操做,而後從新解析該緩存內容,讀取複雜,速度比內存緩存慢。java

以瀏覽器中顯示的說明爲列:web

瀏覽器緩存圖片1.png

  • from memory cache,表明使用內存中的緩存
  • from disk cache,表明使用的是硬盤中的緩存
  • from prefetch cache ,在 preload 或 prefetch 的資源加載時,二者也是均存儲在 http cache,當資源加載完成後,若是資源是能夠被緩存的,那麼其被存儲在 http cache 中等待後續使用;若是資源不可被緩存,那麼其在被使用前均存儲在 memory cache。

瀏覽器資源緩存訪問優先級

瀏覽器請求一個資源時,會按照優先級(Service Worker -> Memory Cache -> Disk Cache -> Push Cache)依次查找緩存,若是命中則使用緩存,不然發起請求。算法

以未開通Service Worker的服務訪問爲例:http://baidu.com–> 200 –> 關閉頁面的標籤頁 –> 從新打開http://baidu.com –> 200(from disk cache) –> 刷新 –> 200(from memory cache)。編程

代理/服務端緩存

網關緩存、CDN、反向代理緩存、負載均衡器等部署在服務器上的緩存瀏覽器

以騰訊 CDN 爲例:請求頭中 X-Cache-Lookup:Hit From MemCache 表示命中 CDN 節點的內存;X-Cache-Lookup:Hit From Disktank 表示命中 CDN 節點的磁盤;X-Cache-Lookup:Hit From Upstream 表示沒有命中 CDN。緩存

瀏覽器資源訪問流程

image2021-4-26_11-48-12.png

關鍵步驟以下:服務器

  1. 瀏覽器發送請求前,會先去緩存裏查看是否命中強緩存,若是命中,則直接從緩存中讀取資源,不會發送請求到服務器。不然,進入下一步。
  2. 當強緩存沒有命中時,瀏覽器向服務器發起請求。
  3. 服務器會根據 Request Header 中的一些字段來判斷是否命中協商緩存。若是命中,服務器會返回 304 響應,可是不會攜帶任何響應實體,只是告訴瀏覽器能夠直接從瀏覽器緩存中獲取這個資源。
  4. 若是本地緩存和協商緩存都沒有命中,則從直接從服務器加載資源,服務器會將緩存規則放入HTTP響應報文的HTTP頭中和請求結果一塊兒返回給瀏覽器

Http協議頭緩存相關字段

強緩存(本地緩存)控制Expires&Cache-Control

制強制緩存的字段分別是請求頭中的Expires和,其中Cache-Control優先級比Expires高。

Expires(響應頭)+Date

Expires是HTTP/1.0控制網頁緩存的響應頭字段,其值爲服務器返回該請求結果緩存的到期時間,即再次發起該請求時,若是客戶端的時間小於Expires的值時,直接使用緩存結果。

Expires是HTTP/1.0的字段,可是如今瀏覽器默認使用的是HTTP/1.1,那麼在HTTP/1.1中網頁緩存仍是否由Expires控制?

到了HTTP/1.1,Expire已經被Cache-Control替代,緣由在於Expires控制緩存的原理是使用客戶端的時間與服務端返回的時間作對比,那麼若是客戶端與服務端的時間由於某些緣由(例如時區不一樣;客戶端和服務端有一方的時間不許確)發生偏差,那麼強制緩存則會直接失效,這樣的話強制緩存的存在則毫無心義。

Cache-Control(請求頭和響應頭都支持這個屬性)

HTTP/1.1定義的 Cache-Control 頭用來區分對緩存機制的支持狀況, 請求頭和響應頭都支持這個屬性。經過它提供的不一樣的值來定義緩存策略。

  • Cache-Control: no-store   //緩存中不得存儲任何關於客戶端請求和服務端響應的內容。每次由客戶端發起的請求都會下載完整的響應內容。
  • Cache-Control: no-cache //強制要求緩存把請求提交給原始服務器進行驗證(協商緩存驗證)。每次有請求發出時,緩存會將此請求發到服務器(該請求應該會帶有與本地緩存相關的驗證字段),服務器端會驗證請求中所描述的緩存是否過時,若未過時(注:實際就是返回304),則緩存才使用本地緩存副本。
  • Cache-Control: private // "private" 則表示該響應是專用於某單個用戶的,中間人不能緩存此響應,該響應只能應用於瀏覽器私有緩存中。
  • Cache-Control: public  //"public" 指令表示該響應能夠被任何中間人(譯者注:好比中間代理、CDN等)緩存。若指定了"public",則一些一般不被中間人緩存的頁面(譯者注:由於默認是private)(好比 帶有HTTP驗證信息(賬號密碼)的頁面 或 某些特定狀態碼的頁面),將會被其緩存。
  • Cache-Control: max-age=31536000   //表示資源可以被緩存(保持新鮮)的最大時間。相對Expires而言,max-age是距離請求發起的時間的秒數。針對應用中那些不會改變的文件,一般能夠手動設置必定的時長以保證緩存有效,例如圖片、css、js等靜態資源。與Age配合使用
  • Cache-Control: must-revalidate    //緩存在考慮使用一個陳舊的資源時,必須先驗證它的狀態,已過時的緩存將不被使用

Pragma(請求頭)

Pragma 是HTTP/1.0標準中定義的一個header屬性,請求中包含Pragma的效果跟在頭信息中定義Cache-Control: no-cache相同,可是HTTP的響應頭沒有明肯定義這個屬性,因此它不能拿來徹底替代HTTP/1.1中定義的Cache-control頭。一般定義Pragma以向後兼容基於HTTP/1.0的客戶端。

協商緩存控制

協商緩存的標識也是在響應報文的HTTP頭中和請求結果一塊兒返回給瀏覽器的,控制協商緩存的字段分別有:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的優先級比Last-Modified / If-Modified-Since高。

Last-Modified(響應頭)與 If-Modified-Since(請求頭)

屬於 http 1.0。當帶着 If-Modified-Since 頭訪問服務器請求資源時,服務器會檢查 Last-Modified,若是 Last-Modified 的時間早於或等於 If-Modified-Since 則會返回一個不帶主體的 304 響應,不然將從新返回資源。

Last-Modified只能精確到一秒,能夠做爲一種弱校驗器。

ETag(響應頭) 與 If-None-Match(請求頭)

屬於 http 1.1。ETag 是一個響應首部字段,強校驗器,它是根據實體內容生成的一段 hash 字符串,標識資源的狀態,由服務端產生。If-None-Match 是一個條件式的請求首部。若是請求資源時在請求首部加上這個字段,值爲以前服務器端返回的資源上的 ETag,則當且僅當服務器上沒有任何資源的 ETag 屬性值與這個首部中列出的時候,服務器纔會返回帶有所請求資源實體的 200 響應,不然服務器會返回不帶實體的 304 響應。

ETag  VS  Last-Modified 

Last-Modified 標註的最後修改只能精確到秒級,若是某些文件在 1 秒鐘之內,被修改屢次的話,它將不能準確標註文件的新鮮度;
某些文件也許會週期性的更改,可是他的內容並不改變(僅僅改變的修改時間),但 Last-Modified 卻改變了,致使文件無法使用緩存;
有可能存在服務器沒有準確獲取文件修改時間,或者與代理服務器時間不一致等情形。
ETag 優先級比 Last-Modified 高,同時存在時會以 ETag 爲準。

Vary (響應頭)

是一個HTTP響應頭部信息,它決定了對於將來的一個請求頭,應該用一個緩存的回覆(response)仍是向源服務器請求一個新的回覆。它被服務器用來代表在 content negotiation algorithm(內容協商算法)中選擇一個資源表明的時候應該使用哪些頭部信息(headers)。它表示某個響應因某個響應頭部而不一樣。

好比 Vary: Accept 的意思即爲,響應因請求資源格式頭部而不一樣,那麼經過相同 URI 訪問的資源就能夠根據這個頭上知道其內容格式不一樣。

同一個 URL 能夠提供多份不一樣的文檔,這就要求服務端和客戶端之間有一個選擇最合適版本的機制,這叫作內容協商。服務端根據客戶端發送的請求頭中某些字段自動發送最合適的版本。能夠用於這個機制的請求頭字段又分兩種:內容協商專用字段(Accept 字段)、其餘字段。

例如:Accept-Encoding 屬於內容協商專用字段,服務端只須要在響應頭中增長 Content-Encoding 字段,用來指明內容壓縮格式;或者不輸出 Content-Encoding 代表內容未通過壓縮。緩存服務器,針對不一樣的 Content-Encoding 緩存不一樣內容,再根據具體請求中的 Accept-Encoding 字段返回最合適的版本。增長 Vary: Accept-Encoding 響應頭,明確告知緩存服務器按照 Accept-Encoding 字段的內容,分別緩存不一樣的版本;

不一樣資源緩存策略

  • 不一樣的資源可能有不一樣的更新要求。審查並肯定每一個資源適合的 max-age;
  • 有些資源的更新比其餘資源頻繁。若是資源的特定部分(例如 JS 函數或一組 CSS 樣式)會常常更新,應考慮將其代碼做爲單獨的文件提供。這樣,每次獲取更新時,剩餘內容(例如不會頻繁更新的庫代碼)能夠從緩存中獲取,確保下載的內容量最少;
  • 對 HTML 文檔組合使用包含內容特徵碼的資源網址以及短期或 no-cache 的生命週期,能夠控制客戶端獲取更新的速度,低頻更新的資源(js/css)變更了,只用在高頻變更的資源文件(html)裏作入口的改動。

特殊說明

F5/點擊工具欄中的刷新按鈕/右鍵菜單從新加載

F5的做用和直接在URI輸入欄中輸入而後回車是不同的,F5會讓瀏覽器不管如何都發一個HTTP Request給Server,即便先前的響應中有Expires頭部。因此,當我在 網頁中按F5的時候,瀏覽器會發送一個HTTP Request給Server,可是包含這樣的Headers:

Cache-Control: max-age=0
If-Modified-Since: Fri, 15 Jul 2016 04:11:51 GMT

其中Cache-Control是Chrome強制加上的,而If-Modified-Since是由於獲取該資源的時候包含了Last-Modified頭部,瀏覽器會使用If-Modified-Since頭部信息從新發送該時間以確認資源是否須要從新發送。 實際上Server沒有修改這個index.css文件,因此返回了一個304(Not Modified),這樣的響應信息很小,所消耗的route-trip很少,網頁很快就刷新了。
瀏覽器緩存圖片3.png

上面的例子中沒有ETag,若是Response中包含ETag,F5引起的Http Request中也是會包含If-None-Match的。

緩存規則實現

以上描述的客戶端瀏覽器緩存是指存儲位置在客戶端瀏覽器, 可是對客戶端瀏覽器緩存的實際設置工做是在服務器上的資源中完成的. 雖然上面介紹了有關於客戶端瀏覽器緩存的屬性, 可是實際上對這些屬性的設置工做都須要在服務器的資源中作設置. 一般有兩種操做手段對瀏覽器緩存進行設置, 一個是經過指令聲明來設置, 另一個是經過編程方式來設置.

Ngnix指令設置

例子1
配置指令expires,能夠控制 HTTP 響應中的Expires和Cache-Controle的值,默認是 off

location ~ .*.(js|css)?$ {    
    expires 1y; }

例2:
對全部後綴爲.html 的請求,返回頭 cache-control 使用 no-cache 指令

location ~ .*\.(html)$ {
         try_files $uri $uri/ =404;
         root /data/web/default;
         add_header Cache-Control no-cache;
}

例3:
etag設置

http {
        etag off;

例4

配置last-modified(默認開啓)

編程方式

這裏再也不詳細描述,不一樣語言做出的server服務可查看相關模塊說明
以koa實現爲例:

//koastart
var koa = require('koa');
var app = new koa();
// response
app.use(function *(){
  this.body = 'Hello World';
  var etag = this.get('ETag');
  console.log("etag:"+etag);
  var date = new Date;
  var hashStr = this.body;
  var hash = require("crypto").createHash('sha1').update(hashStr).digest('base64');
  this.set({
    'Cache-Control':'max-age=120',
    'Etag': hash,
    'Last-Modified': new Date
  });
});
app.listen(3000);

參考文檔

MDN-http緩存:https://developer.mozilla.org... ;
HTTP緩存控制:https://imweb.io/topic/5795dc... ;
https://web.dev/http-cache/ ;
https://mp.weixin.qq.com/s/d2... ;
https://www.jiqizhixin.com/ar... ;

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息