web性能優化:詳說瀏覽器緩存

TOC

  • 背景
  • 瀏覽器的總流程圖
  • 一步一步說緩存
    • 樸素的靜態服務器
    • 設置緩存超時時間
    • html5 Application Cache
    • Last-Modified/If-Modified-Since
    • Etag/If-None-Match
      • 什麼是Etag
      • 爲何有了Last-Modified還要Etag
      • Etag 的實現
  • 迷之瀏覽器
  • 總結

背景

在對頁面的性能優化時,特別是移動端的優化,緩存是很是重要的一環。
瀏覽器緩存機制設置衆多:html5 appcache,Expires,Cache-control,Last-Modified/If-Modified-Since,Etag/If-None-Match,max-age=0/no-cache...,
以前對某一個或幾個特性瞭解一二,可是混在一塊兒再加上瀏覽器的行爲,就迷(meng)糊(bi)了.javascript

下面從實現一個簡單的靜態服務器角度,一步一步說瀏覽器的緩存策略。css

瀏覽器緩存總流程圖

對http請求來講,客戶端緩存分三類:html

  • 不發任何請求,直接從緩存中取數據,表明的特性有: Expires ,Cache-Control=<number!=0>和appcache
  • 發請求確認是否新鮮,再決定是否返回304並從緩存中取數據 :表明的特性有:Last-Modified/If-Modified-Since,Etag/If-None-Match
  • 直接發送請求, 沒有緩存,表明的特性有:Cache-Control:max-age=0/no-cache

時間寶貴,如下是最終的流程圖:html5

源碼和流程圖源文件在githubjava

img

一步一步說緩存

樸素的靜態服務器

瀏覽的緩存的依據是server http response header , 爲了實現對http response 的徹底控制,用nodejs實現了一個簡單的static 服務器,得益於nodejs簡單高效的api,
不到60行就把一個可用的版本實現了:源碼
可克隆代碼,分支切換到step1, 進入根目錄,執行 node app.js,瀏覽器裏輸入:http://localhost:8888/index.html,查看response header,返回正常,也沒有用任何緩存。
服務器每次都要調用fs.readFile方法去讀取硬盤上的文件的。當服務器的請求量一上漲,硬盤IO會成爲性能瓶頸(設置了內存緩存除外)。node

response header:
HTTP/1.1 200 OK
Content-Type: text/html
Date: Fri, 03 Jun 2016 14:15:35 GMT
Connection: keep-alive
Transfer-Encoding: chunked

image

設置緩存超時時間

對於指定後綴文件和過時日期,爲了保證可配置。創建一個config.js。nginx

exports.Expires = {

    fileMatch: /^(gif|png|jpg|js|css|html)$/ig,

    maxAge: 60*60*24*365

};

爲了把緩存這個職責獨立出來,咱們再新建一個cache.js,做爲一箇中間件處理request.git

加上超期時間,代碼以下github

module.exports = function (request, response) {
     var pathname = url.parse(request.url).pathname;
    var ext = path.extname(pathname);
    ext = ext ? ext.slice(1) : 'unknown';

    if (ext.match(config.Expires.fileMatch)) {

        var expires = new Date();

        expires.setTime(expires.getTime() + config.Expires.maxAge * 1000);

        response.setHeader("Expires", expires.toUTCString());
        
        response.setHeader("Cache-Control", "max-age=" + config.Expires.maxAge);

    }
}

這時咱們刷新頁面能夠看到response header 變爲這樣了:web

HTTP/1.1 200 OK
Expires: Sat, 03 Jun 2017 15:07:23 GMT
Cache-Control: max-age=31536000
Content-Type: text/html
Date: Fri, 03 Jun 2016 15:07:23 GMT
Connection: keep-alive
Transfer-Encoding: chunked

多了expires,但這是第一次訪問,流程和上面同樣,仍是須要從硬盤讀文件,再response

再刷新頁面,能夠看到http header :

Request URL:http://127.0.0.1:8888/index.html
Request Method:GET
Status Code:200 OK (from cache)
Remote Address:127.0.0.1:8888

image

可是到這裏遇到一個問題,並無達到預期的效果,並無從緩存讀取

緩存並無生效。

GET /index.html HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8

查看request header 發現 Cache-Control: max-age=0,瀏覽器強制不用緩存。

瀏覽器會針對的用戶不一樣行爲採用不一樣的緩存策略:

Chrome does something quite different: 'Cache-Control' is always set to 'max-age=0′, no matter if you press enter, f5 or ctrl+f5. Except if you start Chrome and enter the url and press enter.
其它的瀏覽器特性能夠查看文末的【迷之瀏覽器】

因此添加文件entry.html,經過連接跳轉的方式進入就能夠看到cache的效果了。

瀏覽器在發送請求以前因爲檢測到Cache-Control和Expires(Cache-Control的優先級高於Expires,但有的瀏覽器不支持Cache-Control,這時採用Expires),
若是沒有過時,則不會發送請求,而直接從緩存中讀取文件。

Cache-Control與Expires的做用一致,都是指明當前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數據仍是從新發請求到服務器取數據。
只不過Cache-Control的選擇更多,設置更細緻,若是同時設置的話,其優先級高於Expires。

代碼詳細可查看源碼:https://github.com/etoah/BrowserCachePolicy/tree/step2

html5 Application Cache

除了Expires 和Cache-Control 兩個特性的緩存可讓browser徹底不發請求的話,別忘了還有一個html5的新特性 Application Cache,
在個人另外一篇文章中有簡單的介紹HTML5 Application cache初探和企業應用啓示.
同時在本身寫的代碼編輯器中,也用到了此特性,可離線查看,坑也比較多。

爲了消除 expires cache-control 的影響,先註釋掉這兩行,並消除瀏覽器的緩存。

// response.setHeader("Expires", expires.toUTCString());
        //response.setHeader("Cache-Control", "max-age=" + config.Expires.maxAge);

新增文件app.manifest,因爲appcache 會緩存當前文件,咱們可不指定緩存文件,只需輸入CACHE MANIFEST,並在entry.html引用這個文件。

<html lang="en" manifest="app.manifest">

在瀏覽器輸入:http://localhost:8888/entry.html,能夠看到appcache ,已經在緩存文件了:
img

從瀏覽器的Resources標籤也能夠看到已緩存的文件:
img

這時再刷新瀏覽器,能夠看到即便沒有 Expires 和Cache-Control 也是 from cache ,

img

而index.html 因爲沒有加Expires ,Cache-Control和appcache 仍是直接從服務器端取文件。

這時緩存的控制以下

img

本例子的源碼爲分支 step3:代碼詳細可查看源碼

Last-Modified/If-Modified-Since

Last-Modified/If-Modified-Since。

  • Last-Modified:標示這個響應資源的最後修改時間。web服務器在響應請求時,告訴瀏覽器資源的最後修改時間。
  • If-Modified-Since:當資源過時時(使用Cache-Control標識的max-age),發現資源具備Last-Modified聲明,則再次向web服務器請求時帶上頭 If-Modified-Since,表示請求時間。web服務器收到請求後發現有頭If-Modified-Since 則與被請求資源的最後修改時間進行比對。若最後修改時間較新,說明資源又被改動過,則響應整片資源內容(寫在響應消息包體內),HTTP 200;若最後修改時間較舊,說明資源無新修改,則響應HTTP 304 (無需包體,節省瀏覽),告知瀏覽器繼續使用所保存的cache。

因此咱們須要把 Cache-Control 設置的儘量的短,讓資源過時:

exports.Expires = {
    fileMatch: /^(gif|png|jpg|js|css|html)$/ig,
    maxAge: 1
};

同時須要識別出文件的最後修改時間,並返回給客戶端,咱們同時也要檢測瀏覽器是否發送了If-Modified-Since請求頭。若是發送並且跟文件的修改時間相同的話,咱們返回304狀態。
代碼以下:

fs.stat(realPath, function (err, stat) {
            var lastModified = stat.mtime.toUTCString();
            var ifModifiedSince = "If-Modified-Since".toLowerCase();
            response.setHeader("Last-Modified", lastModified);
            if (request.headers[ifModifiedSince] && lastModified == request.headers[ifModifiedSince]) {
                response.writeHead(304, "Not Modified");
                response.end();
            }
        })

若是沒有發送或者跟磁盤上的文件修改時間不相符合,則發送回磁盤上的最新文件。

一樣咱們清緩存,刷新兩次就能看到效果以下:

img

服務器請求確認了文件是否新鮮,直接返回header, 網絡負載特別較小:

img

這時咱們的緩存控制流程圖以下:

img

本例子的源碼爲分支 step4:代碼詳細可查看源碼:https://github.com/etoah/BrowserCachePolicy/tree/step4

Etag/If-None-Match

除了有Last-Modified/If-Modified-Since組合,還有Etag/if-None-Match,

什麼是Etag

ETag ,全稱Entity Tag.

  • Etag:web服務器響應請求時,告訴瀏覽器當前資源在服務器的惟一標識(生成規則由服務器決定,具體下文中介紹)。
  • If-None-Match:當資源過時時(使用Cache-Control標識的max-age),發現資源具備Etage聲明,則再次向web服務器請求時帶上頭If-None-Match (Etag的值)。
    web服務器收到請求後發現有頭If-None-Match 則與被請求資源的相應校驗串進行比對,決定返回200或304。

爲何有了Last-Modified還要Etag

你可能會以爲使用Last-Modified已經足以讓瀏覽器知道本地的緩存副本是否足夠新,爲何還須要Etag(實體標識)呢?HTTP1.1中Etag的出現主要是爲了解決幾個Last-Modified比較難解決的問題:

  • Last-Modified標註的最後修改只能精確到秒級,若是某些文件在1秒鐘之內,被修改屢次的話,它將不能準確標註文件的修改時間
  • 若是某些文件會被按期生成,當有時內容並無任何變化,但Last-Modified卻改變了,致使文件無法使用緩存
  • 有可能存在服務器沒有準確獲取文件修改時間,或者與代理服務器時間不一致等情形

Etag 的實現

在node 的後端框架express 中引用的是npm包etag,etag 支持根據傳入的參數支持兩種etag的方式:
一種是文件狀態(大小,修改時間),另外一種是文件內容的哈希值。
詳情可相看etag源碼

由上面的目的,也很容易想到怎麼簡單實現,這裏咱們對文件內容哈希獲得Etag值。
哈希會用到node 中的Crypto模塊 ,先引用var crypto = require('crypto');,並在響應時加上Etag:

var hash = crypto.createHash('md5').update(file).digest('base64');
                    response.setHeader("Etag", hash);
                    if (
                        (request.headers['if-none-match'] && request.headers['if-none-match'] === hash)
                        // ||
                        // (request.headers[ifModifiedSince] && new Date(lastModified) <= new Date(request.headers[ifModifiedSince]))
                    ) {
                        response.writeHead(304, "Not Modified");
                        response.end();
                        return;
                    }

爲了消除 Last-Modified/If-Modified-Since的影響,測試時能夠先註釋此 header,這裏寫的是 strong validator,詳細可查看W3C ETag
第二次訪問時,正常的返回304,並讀取緩存

img

更改文件,etag發生不匹配,返回200

img

還有一部份功能特性,因爲支持度不廣(部份客戶端不支持(chrome,firefox,緩存代理服務器)不支持,或主流服務器不支持,如nginx, Appache)沒有特別的介紹。
到這裏最終主要的瀏程圖已完畢,最終的流程圖:

img

最終代碼可查看源碼

迷之瀏覽器

每一個瀏覽器對用戶行爲(F5,Ctrl+F5,地址欄回車等)的處理都不同,詳細請查看Clientside Cache Control
如下摘抄一段:

So I tried this for different browsers. Unfortunately it's specified nowhere what a browser has to send in which situation.

  • Internet Explorer 6 and 7 do both send only cache refresh hints on ctrl+F5. On ctrl+F5 they both send the header field 'Cache-Control' set to 'no-cache'.
  • Firefox 3 do send the header field 'Cache-Control' with the value 'max-age=0′ if the user press f5. If you press ctrl+f5 Firefox sends the 'Cache-Control' with 'no-cache' (hey it do the same as IE!) and send also a field 'Pragma' which is also set to 'no-cache'.
  • Firefox 2 does send the header field 'Cache-Control' with the value 'max-age=0′ if the user press f5. ctrl+f5 does not work.
  • Opera/9.62 does send 'Cache-Control' with the value 'max-age=0′ after f5 and ctrl+f5 does not work.
  • Safari 3.1.2 behaves like Opera above.
  • Chrome does something quite different: 'Cache-Control' is always set to 'max-age=0′, no matter if you press enter, f5 or ctrl+f5. Except if you start Chrome and enter the url and press enter.

總結

這只是一篇原理或是規則性的文章,初看起來比較複雜,但現實應用可能只用到了不多的一部份特性就能達到較好的效果:
咱們只需在打包的時候用gulp生成md5戳或時間戳,過時時間設置爲10年,更新版本時更新戳,緩存策略簡單高效。
關於緩存配置的實戰這些問題,
好比,appcache,Expires/Cache-Control 都是不需發任何請求,適用於什麼場景,怎麼選擇?
配置時,不是配置express,配的是nginx,怎麼配置 ,下篇《詳說瀏覽器緩存-實戰篇》更新。

Reference

W3C ETag
rfc2616
What takes precedence: the ETag or Last-Modified HTTP header?

出處:http://www.cnblogs.com/etoah/ 歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面保留此段聲明。

相關文章
相關標籤/搜索