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)了.

下面从实现一个简单的静态服务器角度,一步一步说浏览器的缓存策略。

浏览器缓存总流程图

对http请求来说,客户端缓存分三类:

  • 不发任何请求,直接从缓存中取数据,代表的特性有: Expires ,Cache-Control=和appcache
  • 发请求确认是否新鲜,再决定是否返回304并从缓存中取数据 :代表的特性有:Last-Modified/If-Modified-Since,Etag/If-None-Match
  • 直接发送请求, 没有缓存,代表的特性有:Cache-Control:max-age=0/no-cache

时间宝贵,以下是最终的流程图:

源码和流程图源文件在github

一步一步说缓存

朴素的静态服务器

浏览的缓存的依据是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会成为性能瓶颈(设置了内存缓存除外)。

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

设置缓存超时时间

对于指定后缀文件和过期日期,为了保证可配置。建立一个config.js。

exports.Expires = {

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

    maxAge: 60*60*24*365

};

为了把缓存这个职责独立出来,我们再新建一个cache.js,作为一个中间件处理request.

加上超期时间,代码如下


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 变为这样了:

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

但是到这里遇到一个问题,并没有达到预期的效果,并没有从缓存读取

缓存并没有生效。

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 ,已经在缓存文件了:

从浏览器的Resources标签也可以看到已缓存的文件:

这时再刷新浏览器,可以看到即使没有 Expires 和Cache-Control 也是 from cache ,

而index.html 由于没有加Expires ,Cache-Control和appcache 还是直接从服务器端取文件。

这时缓存的控制如下

本例子的源码为分支 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();
            }
        })

如果没有发送或者跟磁盘上的文件修改时间不相符合,则发送回磁盘上的最新文件。

同样我们清缓存,刷新两次就能看到效果如下:

服务器请求确认了文件是否新鲜,直接返回header, 网络负载特别较小:

这时我们的缓存控制流程图如下:

本例子的源码为分支 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,并读取缓存

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

还有一部份功能特性,由于支持度不广(部份客户端不支持(chrome,firefox,缓存代理服务器)不支持,或主流服务器不支持,如nginx, Appache)没有特别的介绍。
到这里最终主要的浏程图已完毕,最终的流程图:

最终代码可查看源码

迷之浏览器

每个浏览器对用户行为(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/
欢迎转载,但未经作者同意必须保留此段声明,且在文章页面保留此段声明。

web性能优化:详说浏览器缓存的更多相关文章

  1. web性能优化之--合理使用http缓存和localStorage做资源缓存

    一.前言 开始先扯点别的: 估计很多前端er的同学应该遇到过:在旧项目中添加新的功能模块.或者修改一些静态文件时候,当代码部署到线上之后,需求方验收OK,此时你送了一口气,当你准备开始得意于自己的ma ...

  2. web性能优化——浏览器相关

    简介 优化是一个持续的过程.所以尽可能的不要有人为的参与.所以能自动化的或者能从架构.框架级别解决的就最更高级别解决. 这样即能实现面对开发人员是透明的.不响应,又能确保所有资源都是被优化过的. 场景 ...

  3. 关于WEB 性能优化 (摘抄)

    压缩源代码和图片 JavaScript文件源代码可以采用混淆压缩的方式,CSS文件源代码进行普通压缩,JPG图片可以根据具体质量来压缩为50%到70%,PNG可以使用一些开源压缩软件来压缩,比如24色 ...

  4. Web性能优化-合并js与css,减少请求

    Web性能优化已经是老生常谈的话题了, 不过笔者也一直没放在心上,主要的原因还是项目的用户量以及页面中的js,css文件就那几个,感觉没什么优化的.人总要进步的嘛,最近在被angularjs吸引着,也 ...

  5. web性能优化 来自《web全栈工程师的自我修养》

    最近在看<web全栈工程师的自我修养>一书,作者是来自腾讯的前端工程师.作者在做招聘前端的时候问应聘者web新能优化有什么了解和经验,应聘者思索后回答“在发布项目之前压缩css和 Java ...

  6. Web性能优化系列

    web性能优化之重要,这里并不打算赘述.本系列课程将带领大家认识.熟悉.深刻体会并且懂得如果去为不同的站点做性能优化 同时,本系列将还会穿插浏览器兼容性相关问题的解决方案,因为在我看来,兼容性同样属于 ...

  7. Linux web性能优化

    1,

  8. [推荐]T- SQL性能优化详解

    [推荐]T- SQL性能优化详解 博客园上一篇好文,T-sql性能优化的 http://www.cnblogs.com/Shaina/archive/2012/04/22/2464576.html

  9. 详解浏览器缓存机制与Apache设置缓存

    一.详解浏览器缓存机制 对于,如何说明缓存机制,在网络上找到了两张图,个人认为思路是比较清晰的.总结时,上图. 这里需要注意的有两点: 1.Last-Modified.Etag是响应头里的数据 2.I ...

随机推荐

  1. 如何dos命令打开服务窗口?

    1.输入services.msc点击<确定>进入服务窗口.如图:

  2. Date类

    Date类 构造方法: Date():根据当前的默认毫秒值创建对象 Date(long date):根据给定的默认毫秒值创建对象 成员方法: public long getTime():获取时间,以毫 ...

  3. oracle查看对象信息

    1.查看某用户下所有对象的信息: SELECT owner, object_type, status, COUNT(*) count# FROM all_objects where owner='xx ...

  4. 【sqlyog(mysql)Test Connection功能实现的原理】

    sqlyog这个软件中有:Test Connection(测试连接)这样的一个功能, 现在我的开发环境是java和mysql,接下来一起探索这个功能的实现过程:

  5. code vs1436 孪生素数 2(数论+素数的判定)

    1436 孪生素数 2  时间限制: 2 s  空间限制: 1000 KB  题目等级 : 白银 Silver 题解  查看运行结果     题目描述 Description 如m=100,n=6 则 ...

  6. 三层架构与MVC &amp;amp; 设计模式的较量

    刚刚学习了三层架构,并且正在实际应用中,但随着学习的深入,又了解到了一个叫MVC的东西,(早在设计模式中就听到过MVC,仅仅是简单查了一下什么意思.)如今正好把这三个东西放在一起聊聊. 三层 是一个分 ...

  7. linq 动态排序,不使用反射

    之前网上搜索的相关方法都是使用了反射的方法来动态获取字段,以实现动态linq排序,但是因为项目组觉得此方法效率低下,所以不予采纳. 所以有了以下代码 public interface IBase{ d ...

  8. Spring引用测试

    上下文 using System; using Spring.Core; using Spring.Aop; using System; using Spring.Core; using Spring ...

  9. Win10的革新与突破

    7月29号win10全球发布,作为微软最后一款操作系统,它会有那些令人期待的革新,作为一个互联网后现代时代,这款操作系统又会融入什么特别元素? 抱着期待,更新了win10后,我花了大概一天的时间,亲身 ...

  10. 我终于解决UM编辑器了 泪......

    气死我了..... 好不容易测试好了....更显得我笨了..... 原来....什么都不用改   只改了2个小位置....真的是.....回首自己 不敢看 0.0 OK  记下步骤  以免以后忘记 将 ...