HTTP 緩存機制詳解

HTTP Cache


什麼是 HTTP Cache

  • 我們知道通過網絡獲取資源緩慢且耗時,需要三次握手等協議與遠程服務器建立通信,對於大點的數據需要多次往返通信大大增加了時間開銷,並且當今流量依舊沒有理想的快速與便宜。對於開發者來說,長久緩存複用重複不變的資源是性能優化的重要組成部分。
  • HTTP 緩存機制就是,配置服務器響應頭來告訴瀏覽器是否應該緩存資源、是否強制校驗緩存、緩存多長時間;瀏覽器非首次請求根據響應頭是否應該取緩存、緩存過期發送請求頭驗證緩存是否可用還是重新獲取資源的過程。下面我們就來結合簡單的 node 服務器代碼(文末)來介紹其中原理。

關鍵字

響應頭 (常用)值 說明
Cache-Control no-cache, no-store, must-revalidate, max-age, public, private 控制瀏覽器是否可以緩存資源、強制緩存校驗、緩存時間
ETag 文件指紋(hash碼、時間戳等可以標識文件是否更新) 強校驗
Last-Modified 請求的資源最近更新時間 弱校驗
Expires 資源緩存過期時間 與響應頭中的 Date 對比
請求頭 說明
If-None-Match 緩存響應頭中的 ETag 值 發送給服務器比對文件是否更新(精確)
If-Modified-Since 緩存響應頭中的 Last-Modified 值 發送給服務器比對文件是否更新(不精確)

簡單流程圖

這裏寫圖片描述

代碼準備

  • index.html
  • img.png
  • server.js

    爲了不影響閱讀代碼貼在頁尾,注意需要自行安裝 mime npm包。

不設置

  • 設置響應頭,則瀏覽器並不能知道是否應該緩存資源,而是每次都發情新的請求,接受新的資源。
// strategy['no-cache'](req, res, filePath, stat);
// strategy['no-store'](req, res, filePath, stat);
// strategy['cache'](req, res, filePath, stat);
strategy['nothing'](req, res, filePath, stat);

$ node server.js
瀏覽器裏輸入:localhost:8080/index.html

  • 首次加載
    這裏寫圖片描述
    這裏寫圖片描述
  • 刷新,每次和上面一樣的效果,都是重新獲取資源。

禁止緩存

  • 設置響應頭

Cache-Control: no-store
或 Cache-Control: no-cache, no-store, must-revalidate

strategy['no-store'](req, res, filePath, stat);

這裏寫圖片描述
效果和不設置一樣,只是明確告訴瀏覽器禁止緩存資源。

private與public

  • Cache-Control: public 表示一些中間代理、CDN等可以緩存資源,即便是帶有一些敏感 HTTP 驗證身份信息甚至響應狀態代碼通常無法緩存的也可以緩存。通常 public 是非必須的,因爲響應頭 max-age 信息已經明確告知可以緩存了。
  • Cache-Control: private 明確告知此資源只能單個用戶可以緩存,其他中間代理不能緩存。原始發起的瀏覽器可以緩存,中間代理不能緩存。例如:百度搜索時,特定搜索信息只能被髮起請求的瀏覽器緩存。
    這裏寫圖片描述

緩存過期策略

一般緩存機制只作用於 get 請求

1、三種方式設置服務器告知瀏覽器緩存過期時間

設置響應頭(注意瀏覽器有自己的緩存替換策略,即便資源過期,不一定被瀏覽器刪除。同樣資源未過期,可能由於緩存空間不足而被其他網頁新的緩存資源所替換而被刪除。):

  • 1、設置 Cache-Control: max-age=1000 //響應頭中的 Date 經過 1000s 過期
  • 2、設置 Expires //此時間與本地時間(響應頭中的 Date )對比,小於本地時間表示過期,由於本地時鐘與服務器時鐘無法保持一致,導致比較不精確
  • 3、如果以上均爲設置,卻設置了 Last-Modified ,瀏覽器隱式的設置資源過期時間爲 (Date - Last-Modified) * 10% 緩存過期時間。

2、兩種方式校驗資源過期

設置請求頭:

  • 1、If-None-Match 如果緩存資源過期,瀏覽器發起請求會自動把原來緩存響應頭裏的 ETag 值設置爲請求頭 If-None-Match 的值發送給服務器用於比較。一般設置爲文件的 hash 碼或其他標識能夠精確判斷文件是否被更新,爲強校驗。
  • 2、If-Modified-Since 同樣對應緩存響應頭裏的 Last-Modified 的值。此值可能取得 ctime 的值,該值可能被修改但文件內容未變,導致對比不準確,爲弱校驗。

下面以常用設置了 Cache-Control: max-age=100If-None-Match 的圖示說明:
這裏寫圖片描述

  • 1、(以下便於測試,未準確設置爲 100s 。)瀏覽器首次發起請求,緩存爲空,服務器響應:
    這裏寫圖片描述

    瀏覽器緩存此響應,緩存壽命爲接收到此響應開始計時 100s 。

  • 2、10s 過後,瀏覽器再次發起請求,檢測緩存未過期,瀏覽器計算 Age: 10 ,然後直接使用緩存,這裏是直接去內存中的緩存,from disk 是取磁盤上的緩存。(這裏不清楚爲什麼,同樣的配置,index.html 文件即便有緩存也 304。
    這裏寫圖片描述

  • 3、100s 過後,瀏覽器再次發起請求,檢測緩存過期,向服務器發起驗證緩存請求。如果服務器對比文件已發生改變,則如 1;否則不返回文件數據報文,直接返回 304。返回 304 時設置 Age: 0 與不設置效果一樣, 猜測是瀏覽器會自動維護。
    這裏寫圖片描述

強制校驗緩存

有時我們既想享受緩存帶來的性能優勢,可有時又不確認資源內容的更新頻度或是其他資源的入口,我們想此服務器資源一旦更新能立馬更新瀏覽器的緩存,這時我們可以設置

Cache-Control: no-cache

這裏寫圖片描述
再次發起請求,無論緩存資源有沒有過期都發起驗證請求,未更新返回 304,否則返回新資源。

性能優化

現在一些單頁面技術,構建工具十分流行。一般一個 html 文件,每次打包構建工具都會動態默認把衆多腳本樣式文件打包成一個 bundle.hashxxx.js 。雖然一個 js 文件看似減少了 HTTP 請求數量,但對於有些三方庫資源等長期不變的資源可以拆分出來,並設置長期緩存,充分利用緩存性能優勢。這時我們完全可以對經常變動的 html 設置 Cache-Control: no-cahce 實時驗證是否更新。而對於鏈接在 html 文件的資源名稱均帶上唯一的文件指紋(時間戳、版本號、文件hash等),設置 max-age 足夠大。資源一旦變動即標識碼也會變動,作爲入口的 html 文件外鏈改變,html 變動驗證返回全新的資源,拉取最新的外鏈資源,達到及時更新的效果。老的資源會被瀏覽器緩存替換機制清除。流程如下:
這裏寫圖片描述

總結:HTTP 緩存性能檢查清單

  • 確保網址唯一:一般瀏覽器以 Request URL 爲鍵值(區分大小寫)緩存資源,不同的網址提供相同的內容會導致多次獲取緩存相同的資源。常見的誤用形式:在網址後面來加個 v=1,例如 https://xxx.com?v=1 來更新新的資源,明顯是對 HTTP 緩存的不理解或配置不當造成的濫用
  • 確保服務器提供了驗證令牌 ETag :提供資源對比機制。
  • 確定中間代理可以緩存哪些資源:對於個人隱私信息可以設置 private,對於公共資源例如 CDN 資源可以設置 public
  • 爲每個資源設置最佳的緩存壽命:max-age 或 Expires,對於不經常變動或不變的資源設置儘可能大的緩存時間,充分利用緩存性能。
  • 確認網站的層次機構:例如單頁面技術,對於頻繁更新的主入口 index.html 文件設置較短的緩存週期或 no-cache 強制緩存驗證,以確保外鏈資源的及時更新。
  • 最大限度減少文件攪動:對於要打包合併的文件,應該儘量區分頻繁、不頻繁變動的文件,避免頻繁變動的內容導致大文件的文件指紋變動(後臺服務器計算文件 hash 很耗性能),儘量頻繁變動的文件小而美,不經常變動例如第三方庫資源可以合併減少HTTP請求數量。

參考

mozilla:HTTP 緩存

谷歌有關性能的文字:HTTP 緩存

node中的緩存機制

w3c Header定義

徹底弄懂 Http 緩存機制 - 基於緩存策略三要素分解法

聽說你用webpack處理文件名的hash?那麼建議你看看你生成的hash對不對

附代碼

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>HTTP Cache</title>
</head>
<body>
    <img src="img.png" alt="流程圖">
    <!-- <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> -->
</body>
</html>

server.js

let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');// 非 node 內核包,需 npm install
let crypto = require('crypto');

// 緩存策略
const strategy = {
    'nothing': (req, res, filePath) => {
        fs.createReadStream(filePath).pipe(res);
    },
    'no-store': (req, res, filePath, stat) => {
        // 禁止緩存
        res.setHeader('Cache-Control', 'no-store');
        // res.setHeader('Cache-Control', ['no-cache', 'no-store', 'must-revalidate']);
        // res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toUTCString());
        // res.setHeader('Last-Modified', stat.ctime.toGMTString());

        fs.createReadStream(filePath).pipe(res);
    },
    'no-cache': (req, res, filePath, stat) => {
        // 強制確認緩存
        // res.setHeader('Cache-Control', 'no-cache');
        strategy['cache'](req, res, filePath, stat, true);
        // fs.createReadStream(filePath).pipe(res);
    },
    'cache': (req, res, filePath, stat, revalidate) => {
        let ifNoneMatch = req.headers['if-none-match'];
        let ifModifiedSince = req.headers['if-modified-since'];
        let LastModified = stat.ctime.toGMTString();
        let maxAge = 30;

        new Promise((resolve, reject) => {
            // 生成文件 hash
            let out = fs.createReadStream(filePath);
            let md5 = crypto.createHash('md5');
            out.on('data', function (data) {
                md5.update(data)
            });
            out.on('end', function () {
                let etag = md5.digest('hex');
                resolve(etag);
            });
        }).then( etag => {
            if ( ifNoneMatch ) {
                if (ifNoneMatch == etag) {
                    console.log('304');
                    // res.setHeader('Cache-Control', 'max-age=' + maxAge);
                    // res.setHeader('Age', 0);
                    res.writeHead('304');
                    res.end();
                } else {
                    // 設置緩存壽命
                    res.setHeader('Cache-Control', 'max-age=' + maxAge);
                    res.setHeader('Etag', etag);
                    fs.createReadStream(filePath).pipe(res);
                }
            }
            /*else if ( ifModifiedSince ) {
                if (ifModifiedSince == LastModified) {
                    res.writeHead('304');
                    res.end();
                } else {
                    res.setHeader('Last-Modified', stat.ctime.toGMTString());
                    fs.createReadStream(filePath).pipe(res);
                }
            }*/
            else {
                // 設置緩存壽命
                console.log('首次響應!');
                res.setHeader('Cache-Control', 'max-age=' + maxAge);
                res.setHeader('Etag', etag);
                // res.setHeader('Last-Modified', stat.ctime.toGMTString());

                revalidate && res.setHeader('Cache-Control', [
                    'max-age=' + maxAge,
                    'no-cache'
                ]);
                fs.createReadStream(filePath).pipe(res);
            }
        });
    }

};

http.createServer((req, res) => {
    console.log( new Date().toLocaleTimeString() + ':收到請求')
    let { pathname } = url.parse(req.url, true);
    let filePath = path.join(__dirname, pathname);
    // console.log(filePath);
    fs.stat(filePath, (err, stat) => {
        if (err) {
            res.setHeader('Content-Type', 'text/html');
            res.setHeader('404', 'Not Found');
            res.end('404 Not Found');
        } else {
            res.setHeader('Content-Type', mime.getType(filePath));

            strategy['no-cache'](req, res, filePath, stat);
            //strategy['no-store'](req, res, filePath, stat);
            // strategy['cache'](req, res, filePath, stat);
            // strategy['nothing'](req, res, filePath, stat);
        }
    });
})
.on('clientError', (err, socket) => {
    socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
})
.listen(8080);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章