瀏覽器緩存一探究竟~

img

先看一張經典的流程圖,結合理解html

吃了它~前端

img

1. 緩存做用

  • 減小了冗餘的數據傳輸,節省了網費。
  • 減小了服務器的負擔, 大大提升了網站的性能
  • 加快了客戶端加載網頁的速度

2. 緩存分類

2.1 DNS 緩存

主要就是在瀏覽器本地把對應的 IP 和域名關聯起來,這樣在進行 DNS 解析的時候就很快。git

2.2 MemoryCache

是指存在內存中的緩存。從優先級上來講,它是瀏覽器最早嘗試去命中的一種緩存。從效率上來講,它是響應速度最快的一種緩存。 內存緩存是快的,也是「短命」的。它和渲染進程「生死相依」,當進程結束後,也就是 tab 關閉之後,內存裏的數據也將不復存在。github

2.3 瀏覽器緩存

瀏覽器緩存,也稱Http 緩存,分爲強緩存協商緩存。優先級較高的是強緩存,在命中強緩存失敗的狀況下,纔會走協商緩存瀏覽器

2.3.1 強緩存

強緩存是利用 http 頭中的 ExpiresCache-Control 兩個字段來控制的。強緩存中,當請求再次發出時,瀏覽器會根據其中的 Expirescache-control 判斷目標資源是否「命中」強緩存,若命中則直接從緩存中獲取資源,不會再與服務端發生通訊。緩存

Expires

實現強緩存,過去咱們一直用Expires。當服務器返回響應時,在 Response Headers 中將過時時間寫入 Expires 字段。像這樣服務器

expires: Wed, 12 Sep 2019 06:12:18 GMTmarkdown

能夠看到,expires 是一個時間戳,接下來若是咱們試圖再次向服務器請求資源,瀏覽器就會先對比本地時間和 expires 的時間戳,若是本地時間小於 expires 設定的過時時間,那麼就直接去緩存中取這個資源。網絡

從這樣的描述中你們也不難猜想,expires 是有問題的,它最大的問題在於對本地時間的依賴。若是服務端和客戶端的時間設置可能不一樣,或者我直接手動去把客戶端的時間改掉,那麼 expires 將沒法達到咱們的預期。session

Cache-Control

考慮到 expires 的侷限性,HTTP1.1 新增了Cache-Control字段來完成 expires 的任務。expires 能作的事情,Cache-Control 都能作;expires 完成不了的事情,Cache-Control 也能作。所以,Cache-Control 能夠視做是 expires 的徹底替代方案。在當下的前端實踐裏,咱們繼續使用 expires 的惟一目的就是向下兼容。

Cache-Control 中,咱們經過max-age來控制資源的有效期。max-age 不是一個時間戳,而是一個時間長度。在本例中,max-age31536000 秒,它意味着該資源在 31536000 秒之內都是有效的,完美地規避了時間戳帶來的潛在問題。

Cache-Control 相對於 expires 更加準確,它的優先級也更高。當 Cache-Control 與 expires 同時出現時,咱們以 Cache-Control 爲準。

能夠參考下下面兩張圖:

img

2.3.2 協商緩存(對比緩存)

協商緩存依賴於服務端與瀏覽器之間的通訊。協商緩存機制下,瀏覽器須要向服務器去詢問緩存的相關信息,進而判斷是從新發起請求、下載完整的響應,仍是從本地獲取緩存的資源。若是服務端提示緩存資源未改動(Not Modified),資源會被重定向到瀏覽器緩存,這種狀況下網絡請求對應的狀態碼是 304。

協商緩存的實現,從 Last-ModifiedEtag,Last-Modified 是一個時間戳,若是咱們啓用了協商緩存,它會在首次請求時隨着 Response Headers 返回:

Last-Modified

Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT
複製代碼

隨後咱們每次請求時,瀏覽器的請求頭 headers 會帶上一個叫 If-Modified-Since 的時間戳字段,它的值正是上一次 response 返回給它的 Last-Modified 值:

If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT
複製代碼

服務器接收到這個時間戳後,會比對該時間戳和資源在服務器上的最後修改時間是否一致,從而判斷資源是否發生了變化。若是發生了變化,就會返回一個完整的響應內容,並在 Response Headers 中添加新的 Last-Modified 值;不然,返回 304 響應,Response Headers 不會再添加 Last-Modified 字段。

以下圖:

img

經過最後修改時間來判斷緩存是否可用

  • Last-Modified:響應時告訴客戶端此資源的最後修改時間
  • If-Modified-Since:當資源過時時(使用 Cache-Control 標識的 max-age),發現資源具備 Last-Modified 聲明,則再次向服務器請求時帶上頭 If-Modified-Since
  • 服務器收到請求後發現有頭 If-Modified-Since 則與被請求資源的最後修改時間進行比對。若最後修改時間較新,說明資源又被改動過,則響應最新的資源內容並返回 200 狀態碼;
  • 若最後修改時間和 If-Modified-Since 同樣,說明資源沒有修改,則響應 304 表示未更新,告知瀏覽器繼續使用所保存的緩存文件。

看個實例代碼:

let http = require('http');
let fs = require('fs');
let path = require('path');
let mime = require('mime');
http.createServer(function (req, res) {
    let file = path.join(__dirname, req.url);
    fs.stat(file, (err, stat) => {
        if (err) {
            sendError(err, req, res, file, stat);
        } else {
            let ifModifiedSince = req.headers['if-modified-since'];
            if (ifModifiedSince) {
                if (ifModifiedSince == stat.ctime.toGMTString()) {
                    res.writeHead(304);
                    res.end();
                } else {
                    send(req, res, file, stat);
                }
            } else {
                send(req, res, file, stat);
            }
        }
    });
}).listen(8080);
function send(req, res, file, stat) {
    res.setHeader('Last-Modified', stat.ctime.toGMTString());
    res.writeHead(200, { 'Content-Type': mime.getType(file) });
    fs.createReadStream(file).pipe(res);
}
function sendError(err, req, res, file, stat) {
    res.writeHead(400, { "Content-Type": 'text/html' });
    res.end(err ? err.toString() : "Not Found");
複製代碼

使用 Last-Modified 存在一些弊端,這其中最多見的就是這樣幾個場景 1. 某些服務器不能精確獲得文件的最後修改時間, 這樣就沒法經過最後修改時間來判斷文件是否更新了。 2. 咱們編輯了文件,但文件的內容沒有改變。服務端並不清楚咱們是否真正改變了文件,它仍然經過最後編輯時間進行判斷。所以這個資源在再次被請求時,會被當作新資源,進而引起一次完整的響應——不應從新請求的時候,也會從新請求。 3. 當咱們修改文件的速度過快時(好比花了 100ms 完成了改動),因爲 If-Modified-Since 只能檢查到以爲最小計量單位的時間差,因此它是感知不到這個改動的——該從新請求的時候,反而沒有從新請求了。 4. 若是一樣的一個文件位於多個CDN服務器上的時候內容雖然同樣,修改時間不同。

第二和第三這兩個場景其實指向了同一個 bug——服務器並無正確感知文件的變化。爲了解決這樣的問題,Etag 做爲 Last-Modified 的補充出現了。

Etag

這個是協商緩存中的另一種

Etag 是由服務器爲每一個資源生成的惟一的標識字符串(指紋),這個標識字符串能夠是基於文件內容編碼的,只要文件內容不一樣,它們對應的 Etag 就是不一樣的,反之亦然。所以 Etag 可以精準地感知文件的變化。

Etag是 Web 服務端產生的,而後發給瀏覽器客戶端。生成過程須要服務器額外付出開銷,會影響服務端的性能,這是它的弊端。所以啓用 Etag 須要咱們審時度勢。正如咱們剛剛所提到的——Etag 並不能替代 Last-Modified,它只能做爲 Last-Modified 的補充和強化存在。

執行流程是這樣的: 1. 客戶端想判斷緩存是否可用能夠先獲取緩存中文檔的ETag,而後經過If-None-Match發送請求給 Web 服務器詢問此緩存是否可用。 2. 服務器收到請求,將服務器的中此文件的ETag,跟請求頭中的If-None-Match相比較,若是值是同樣的,說明緩存仍是最新的,Web 服務器將發送304 Not Modified響應碼給客戶端表示緩存未修改過,可使用。 3. 若是不同則 Web 服務器將發送該文檔的最新版本給瀏覽器客戶端

看以下實例代碼:

let http = require("http");
let fs = require("fs");
let path = require("path");
let mime = require("mime");
let crypto = require("crypto");
http
  .createServer(function(req, res) {
    let file = path.join(__dirname, req.url);
    fs.stat(file, (err, stat) => {
      if (err) {
        sendError(err, req, res, file, stat);
      } else {
        let ifNoneMatch = req.headers["if-none-match"];
        let etag = crypto
          .createHash("sha1")
          .update(stat.ctime.toGMTString() + stat.size)
          .digest("hex");
        if (ifNoneMatch) {
          if (ifNoneMatch == etag) {
            res.writeHead(304);
            res.end();
          } else {
            send(req, res, file, etag);
          }
        } else {
          send(req, res, file, etag);
        }
      }
    });
  })
  .listen(8080);
function send(req, res, file, etag) {
  res.setHeader("ETag", etag);
  res.writeHead(200, { "Content-Type": mime.lookup(file) });
  fs.createReadStream(file).pipe(res);
}
function sendError(err, req, res, file, etag) {
  res.writeHead(400, { "Content-Type": "text/html" });
  res.end(err ? err.toString() : "Not Found");
}
複製代碼

強緩存和協商緩存比較

優先級:

Etag 在感知文件變化上比 Last-Modified 更加準確,優先級也更高。當 EtagLast-Modified 同時存在時,以 Etag 爲準。

對比:

  • 強制緩存若是生效,不須要再和服務器發生交互,而對比緩存不論是否生效,都須要與服務端發生交互
  • 兩類緩存規則能夠同時存在,強制緩存優先級高於對比緩存,也就是說,當執行強制緩存的規則時,若是緩存生效,直接使用緩存,再也不執行對比緩存規則

2.4 Service Worker Cache

Service Worker 是一種獨立於主線程以外的 Javascript 線程。它脫離於瀏覽器窗體,所以沒法直接訪問 DOM。這樣獨立的個性使得 Service Worker 的「我的行爲」沒法干擾頁面的性能,這個幕後工做者能夠幫咱們實現離線緩存消息推送網絡代理等功能。咱們藉助 Service worker 實現的離線緩存就稱爲 Service Worker Cache。

Service Worker 的生命週期包括 install、activited、working 三個階段。一旦 Service Worker 被 install,它將始終存在,只會在 active 與 working 之間切換,除非咱們主動終止它。這是它能夠用來實現離線存儲的重要先決條件.

它就在瀏覽器開發工具(F12) Application 標籤頁中

2.5 Push Cache

Push Cache 是指 HTTP2server push 階段存在的緩存。這塊的知識比較新,應用也還處於萌芽階段,應用範圍有限不表明不重要——HTTP2 是趨勢、是將來。在它還未被推而廣之的此時此刻,仍但願你們能對 Push Cache 的關鍵特性有所瞭解:

  • Push Cache 是緩存的最後一道防線。瀏覽器只有在 Memory CacheHTTP CacheService Worker Cache 均未命中的狀況下才會去詢問 Push Cache
  • Push Cache 是一種存在於會話階段的緩存,當 session 終止時,緩存也隨之釋放。
  • 不一樣的頁面只要共享了同一個 HTTP2 鏈接,那麼它們就能夠共享同一個 Push Cache

3. 請求流程

3.1 第一次請求

img

3.2 第二次請求

走上面的緩存機制

4. 如何幹脆不發請求

  • 瀏覽器會將文件緩存到Cache目錄,第二次請求時瀏覽器會先檢查Cache目錄下是否含有該文件,若是有,而且還沒到Expires設置的時間,即文件尚未過時,那麼此時瀏覽器將直接從 Cache 目錄中讀取文件,而再也不發送請求
  • Expires是服務器響應消息頭字段,在響應 http 請求時告訴瀏覽器在過時時間前瀏覽器能夠直接從瀏覽器緩存取數據,而無需再次請求,這是HTTP1.0的內容,如今瀏覽器均默認使用HTTP1.1,因此基本能夠忽略
  • Cache-ControlExpires的做用一致,都是指明當前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數據仍是從新發請求到服務器取數據,若是同時設置的話,其優先級高於Expires

5.1 使用 Cache-Control

  • private 客戶端能夠緩存
  • public 客戶端和代理服務器均可以緩存
  • max-age=60 緩存內容將在 60 秒後失效
  • no-cache 須要使用對比緩存驗證數據,強制向源服務器再次驗證
  • no-store 全部內容都不會緩存,強制緩存對比緩存都不會觸發
  • Cache-Control:private, max-age=60, no-cache
let http = require("http");
let fs = require("fs");
let path = require("path");
let mime = require("mime");
let crypto = require("crypto");
http
  .createServer(function(req, res) {
    let file = path.join(__dirname, req.url);
    console.log(file);

    fs.stat(file, (err, stat) => {
      if (err) {
        sendError(err, req, res, file, stat);
      } else {
        send(req, res, file);
      }
    });
  })
  .listen(8080);
function send(req, res, file) {
  let expires = new Date(Date.now() + 60 * 1000);
  res.setHeader("Expires", expires.toUTCString());
  res.setHeader("Cache-Control", "max-age=60");
  res.writeHead(200, { "Content-Type": mime.lookup(file) });
  fs.createReadStream(file).pipe(res);
}
function sendError(err, req, res, file, etag) {
  res.writeHead(400, { "Content-Type": "text/html" });
  res.end(err ? err.toString() : "Not Found");
}
複製代碼

參考資料

談談瀏覽器緩存

最後

若是本文對你有幫助的話,給本文點個贊吧

鄙人github,一塊兒學習~

相關文章
相關標籤/搜索