前端本地緩存概況之瀏覽器緩存策略

引子

一直以來,前端性能優化 都是前端程序員在業務開發過程當中不得不考慮的一個點。前端同窗也一直寄但願於服務器更大的吞吐量、更密集的cdn節點;更寄但願於瀏覽用戶使用更優秀的瀏覽器及更大的帶寬。。。然而隨着上述幾種狀況一一被落實時,前端性能仍然沒有達到一個讓人滿意的結果。。。javascript

此過程當中,前端人就自身狀況也進行了多種嘗試,其中前端本地緩存能夠說是性能優化中簡單高效的一種方式,該方式縮短了網頁請求資源的時長,此外緩存文件能夠複用,則進一步減小了網絡請求次數,提升了頁面加載效率。css

前言

首先咱們要明確一點:瀏覽器和服務器進行通信屬於 請求/應答式html

簡短鏈路爲前端

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

瀏覽器緩存就是把一個已經請求過的Web資源(如html頁面,圖片,js,數據等)儲存在本地(內存或者硬盤)。當下一次請求要發出的時候,若是是相同的URL,瀏覽器會根據緩存機制決定是直接使用先前存儲的資源,仍是向源服務器再次發送請求。同時而並非全部請求都須要存儲於本地(好比數據接口),那麼既然咱們要在瀏覽器層緩存特定資源,應該怎樣進行判斷或者約定?node

緩存分類

一、強緩存

強緩存不會向服務器發送請求,直接從緩存中讀取資源,在瀏覽器控制檯的 Network 選項中能夠看到該請求返回200的狀態碼,而且 Size 顯示 from disk cachefrom memory cache等。程序員

強緩存能夠經過設置兩種 HTTP Header 實現:ExpiresCache-Controlweb

memory cache(內存)

內存緩存,主要包含頁面中已經獲取到的資源,好比頁面的腳本文件、樣式文件、圖片等,內存的讀取速度要比磁盤快。該緩存屬於 會話級別,一但會話結束,則緩存資源被釋放。此外內存容量所限,該緩存更多存儲小體積的請求資源。算法

該緩存主要關注當前會話中第一次請求返回中,response-headers 中的 cache-controlExpires 兩個字段狀況,一經識別計算經過,則資源將被存儲於內存中。(通常cdn都會配置該策略)json

一、Expires

緩存過時時間,用來指定資源到期的時間,是服務器端的具體的時間點。也就是:Expires = max-age + 到期時間(該到期時間爲絕對時間)。Expires 是Web服務器響應消息頭字段,在響應http請求時告訴瀏覽器在過時時間前,瀏覽器能夠直接從瀏覽器緩存取數據,而無需再次請求。

ExpiresHTTP/1.0 的產物,受限於本地時間,若是修改了本地時間,可能會形成緩存失效。

二、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、圖片等

二、協商緩存(對比緩存)

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

備註

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

304

Http 304 狀態請求

文件有更新,協商緩存失效,返回200及相關數據資源

文件未更新,協商緩存生效,返回304和空響應,瀏覽器直接讀取緩存資源

image.png

如圖所示,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,那麼咱們下面用一張流線圖描述下請求及緩存過程:

image.png

測試代碼

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 + '/');

熱門文章推薦

最後

  • 歡迎您加我微信【 ypxiaozhan01】,拉您進技術羣,一塊兒交流學習...
  • 歡迎您關注【 YP小站】,學習互聯網最流行的技術,作個專業的技術人...

  【文章讓您有收穫,👇  或者 在看 支持我吧】

本文分享自微信公衆號 - YP小站(ypxiaozhan)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索