前端本地快取概況之瀏覽器快取策略

語言: CN / TW / HK

一直以來, 前端效能優化 都是前端程式設計師在業務開發過程中不得不考慮的一個點。前端同學也一直寄希望於伺服器更大的吞吐量、更密集的cdn節點;更寄希望於瀏覽使用者使用更優秀的瀏覽器及更大的頻寬。。。然而隨著上述幾種情況一一被落實時,前端效能仍然沒有達到一個讓人滿意的結果。。。

此過程中,前端人就自身情況也進行了多種嘗試,其中前端本地快取可以說是效能優化中簡單高效的一種方式,該方式縮短了網頁請求資源的時長,此外快取檔案可以複用,則進一步減少了網路請求次數與實踐,提高了頁面載入效率。

前言

首先我們要明確一點:瀏覽器和伺服器進行通訊屬於 請求/應答式

簡短鏈路為

發起請求 -> 伺服器響應請求 -> 獲取資源。

瀏覽器快取就是把一個已經請求過的Web資源(如html頁面,圖片,js,資料等)儲存在本地(記憶體或者硬碟)。當下一次請求要發出的時候,如果是相同的URL,瀏覽器會根據快取機制決定是直接使用先前儲存的資源,還是向源伺服器再次傳送請求。同時而並不是所有請求都需要儲存於本地(比如資料介面),那麼既然我們要在瀏覽器層快取特定資源,應該怎樣進行判斷或者約定?

快取分類

1、強快取

強快取不會向伺服器傳送請求,直接從快取中讀取資源,在瀏覽器控制檯的 Network 選項中可以看到該請求返回200的狀態碼,並且 Size 顯示 from disk cachefrom memory cache 等。

強快取可以通過設定兩種 HTTP Header 實現: ExpiresCache-Control

memory cache(記憶體)

記憶體快取,主要包含頁面中已經獲取到的資源,比如頁面的指令碼檔案、樣式檔案、圖片等,記憶體的讀取速度要比磁碟快。該快取屬於 會話級別 ,一但會話結束,則快取資源被釋放。此外記憶體容量所限,該快取更多儲存小體積的請求資源。

該快取主要關注當前會話中第一次請求返回中, response-headers 中的 cache-controlExpires 兩個欄位情況,一經識別計算通過,則資源將被儲存於記憶體中。(一般cdn都會配置該策略)

1、Expires

快取過期時間,用來 指定資源到期的時間,是伺服器端的具體的時間點 。也就是:Expires = max-age + 到期時間(該到期時間為絕對時間)。 Expires 是Web伺服器響應訊息頭欄位,在響應http請求時告訴瀏覽器在過期時間前,瀏覽器可以直接從瀏覽器快取取資料,而無需再次請求。

ExpiresHTTP/1.0 的產物,受限於本地時間,如果修改了本地時間,可能會造成快取失效。

2、Cache-Control

在HTTP/1.1中, Cache-Control 是最重要的規則,主要用於控制網頁快取。比如當 Cache-Control:max-age=300 時,則代表在這個請求正確返回時間(瀏覽器也會記錄下來)的300秒內再次請求資源,就會命中強快取。

備註:

  1. ExpiresCache-Control 的區別在於 Expires 是 http1.0 的產物,Cache-Control 是http1.1 的產物,兩者同時存在的話,Cache-Control 優先順序高於 Expires;Expires 主要是用來相容HTTP1.0,已經過時且存在弊端:

  2. Expires 時間根據本地時間而來,如果改變本地的時間。有可能會使當前的 Expires 快取失效。 強快取判斷是否快取的依據來自於是否超出某個時間或者某個時間段,而不關心伺服器端檔案是否已經更新,這可能會導致載入檔案不是伺服器端最新的內容。

disk cache(硬碟)

其具體快取機制與 memory cache 一致,只是可以儲存的資料量比較大,但是相對來說讀取略慢,比較記憶體快取來說,硬碟快取的優點主要體現在時效性上和容量上。硬碟快取中存入的資料也是根據 http header 中的欄位判定的。哪些資源可以進行儲存,哪些不進行儲存。

disk cache 不同於 memory cache ,disk cache的資源是從磁碟當中取出的,也是在已經在之前的某個時間載入過該資源,不會請求伺服器,但是此資源不會隨著該頁面的關閉而釋放掉,因為是存在硬碟當中的,下次開啟仍會 from disk cache

那麼,問題來了,既然 memory cache 和 disk cache 機制一致,那麼哪些資源會放在記憶體當中,哪些資源瀏覽器會放在磁碟上呢?市面上不同瀏覽器有不同策略機制,以下以Chrome瀏覽器採取的策略簡單描述一下:

狀態 型別 說明
200 form memory cache 不請求網路資源,資源在記憶體當中,一般指令碼、字型會存在記憶體當中
200 form disk ceche 不請求網路資源,在磁碟當中,一般非指令碼會存在記憶體當中,如css、圖片等

2、協商快取(對比快取)

協商快取就是 強制快取失效 後,瀏覽器攜帶快取標識向伺服器發起請求,由伺服器根據快取標識決定是否使用快取的過程,需要強調的是,這個過程是 需要發出請求的

備註

強制快取優先於協商快取進行,若強制快取 (Expires 和 Cache-Control) 生效則直接使用快取,若不生效則進行協商快取( Last-Modified / If-Modified-Since 和 Etag / If-None-Match),協商快取由伺服器決定是否使用快取,若協商快取失效,那麼代表該請求的快取失效,返回200,重新返回資源和快取標識,再存入瀏覽器快取中;生效則返回304,繼續使用快取。

304

Http 304 狀態請求

檔案有更新,協商快取失效,返回200及相關資料資源

檔案未更新,協商快取生效,返回304及空響應及,瀏覽器直接讀取快取資源

如圖所示,http請求攜帶的快取標識可以有兩個,分別是 Last-modifiedEtag ,接下來我們慢慢說一說這兩個。

Last-modifiedif-Modified-since

Last-modified :最後的修改時間,根據比對修改時間可以確定在這一段時間裡資源是否進行了修改。

最小顆粒為 S ,這顆粒度也就暴露了這個屬性的弊端,如果在一秒以內修改多次,則資料不會更新。

瀏覽器第一次請求的時候,響應資源的 header 中新增 last-modified ,數值為資源在伺服器的最後修改時間。瀏覽器下一次請求的時候,檢測到先前返回 header 中有 last-modified 屬性,則請求上行時 header 中會新增 if-modified-since 屬性,值與 last-modified 一致。

伺服器再次收到這個資源請求,會根據 If-Modified-Since 中的值與伺服器中這個資源的最後修改時間對比,如果兩個值相等,返回狀態碼 304空的響應體 ,直接約定從瀏覽器快取中讀取;如果 If-Modified-Since 的時間小於伺服器中這個資源的最後修改時間,說明檔案有更新,於是返回 新的資原始檔 和狀態碼 200

當然這個 last-modifiedhttp1.0 年代的產物,存在著重大弊端,因為 Last-Modified 只能 以秒計時 ,如果在同一個秒時間內修改了檔案,那麼此時二次請求服務端,服務端會認為資源未變更,進而返回304,造成資源錯誤。

Etagif-none-macth

前面說到了 Last-modifiedif-Modified-since 組合的弊端,於是在後續的http1.1 版本,引入了 Etagif-none-macth 組合。

Etag 是伺服器響應請求時,返回當前資原始檔的一個唯一標識,一般是一個 hash值 ,只要資源有變化,Etag就會重新生成。

瀏覽器在下一次載入資源向伺服器傳送請求時,會將上一次返回的 Etag值 放到請求上行的 headerIf-None-Match 屬性裡,伺服器只需要比較客戶端傳來的 header If-None-Match 值跟自己伺服器上該資源的Etag是否一致,就能直接判斷資源相對客戶端快取而言是否有修改。如果伺服器發現Etag匹配不上,那麼直接返回狀態碼200及新資源(當然也包括了新的Etag);如果匹配是一致的,則直接返回304和空的響應體,直接約定從瀏覽器快取中讀取。這裡就避免了 last-modified秒級誤差問題 。至此,我們已經介紹了3種快取: memory cachedisk cache304 ,那麼我們下面用一張流線圖描述下請求及快取過程:

測試程式碼

const http = require('http');//node自帶http server處理模組
const url = require('url');//node自帶路url理模組
const fs = require('fs');//node自帶檔案處理模組
const path = require('path');//node自帶路徑處理模組
const crypto = require("crypto");//node自帶通用加密/雜湊演算法模組
const PORT = 8088;//server 埠號
const mime = {
    "css": "text/css",
    "gif": "image/gif",
    "html": "text/html",
    "ico": "image/x-icon",
    "jpeg": "image/jpeg",
    "jpg": "image/jpeg",
    "js": "text/javascript",
    "json": "application/json",
    "pdf": "application/pdf",
    "png": "image/png",
    "svg": "image/svg+xml",
    "swf": "application/x-shockwave-flash",
    "tiff": "image/tiff",
    "txt": "text/plain",
    "wav": "audio/x-wav",
    "wma": "audio/x-ms-wma",
    "wmv": "video/x-ms-wmv",
    "xml": "text/xml"
};//單體-response檔案型別

/** 生成hash值 */
const getHash = function (str) {
    const shasum = crypto.createHash('sha1');
    return shasum.update(str).digest('base64');
};
const server=new http.Server();//建立server
/** 監聽server 請求 */
server.on("request",function(req,res){
    const pathname = url.parse(req.url).pathname;
    const realPath = path.join(__dirname, pathname);
    let ext = path.extname(pathname);
    ext = ext ? ext.slice(1) : 'unknown';//獲取請求檔案字尾
    const contentType = mime[ext] || "text/plain";
    //判斷檔案狀態,當然也可以用fs.exists()方法,但此處需要讀取檔案修改時間,必須使用stat
    fs.stat(realPath, (error,stat) => {
        console.log('檔案請求:' + pathname);
        if (!error) {
            //根據路徑讀取server端資源
            fs.readFile(realPath, "binary", (err, file) => {
                if (err) {
                    res.writeHead(500, {
                        'Content-Type': 'text/plain'
                    });
                    res.end(JSON.stringify(err));
                    console.log('500錯誤:' + pathname);
                } else {
                    const hash = getHash( file ); //require("crypto").createHash('sha1').update(pathname).digest('base64');
                    const lastModified = stat.mtime.toUTCString();//server端對應檔案最後修改日期
                    if( req.headers['if-none-match'] === hash || req.headers['if-modified-since'] === lastModified ){
                        res.writeHead(304);//Etag或Last-modified一致,直接返回304及空體
                        res.end();
                        return;
                    }
                    res.writeHead(200, {
                        'Content-Type': contentType,
                        // 'Cache-Control': 'max-age=1000',//設定快取1000秒
                        // 'Expires': new Date('Fri May 27 2020 14:53:17 GMT+0800').toUTCString(),//設定具體快取到期時間
                        // "Last-Modified": lastModified,//宣告最後檔案修改時間
                        'Etag': hash//宣告檔案雜湊值
                    });
                    res.write(file, "binary");
                    res.end();
                }
            });
        } else {
            res.writeHead(404, {
                'Content-Type': 'text/plain'
            });
            res.write('[404] This request URL [' + pathname + ']' + '  was not found on this server. [404]');
            res.end();
            console.log('404錯誤:' + pathname);
        }
    })
});
server.listen(PORT);
console.log('Server running at http://127.0.0.1:' + PORT + '/');

---本文結束感謝您的閱讀。 微信掃描二維碼,關注我的公眾號---