聊聊浏览器缓存
一.引言
对于前端工程化,浏览器缓存是一个很重要的知识点。
浏览器缓存是提升加载性能的重要手段,但如果使用不当,反而会造成副作用,
比如:脚本发布了但客户端没加载最新的
下面就对浏览器缓存一探究竟
二. 浏览器缓存类型
浏览器缓存类型分两种:
协商缓存:每次读取缓存时,先到服务器端去验证一下是否有改变,如果有就获取新的,没有从缓存中读取,响应code为304
强制缓存:只要缓存没过有效期,就强制读取缓存,响应code为200。该类缓存必定是要持久化到disk的。Firefox中并没有对强制缓存进行进一步的分类。但谷歌浏览器的处理略有不同,分为两种:一种是(from disk cache),另一种是(from memory cache)
谷歌浏览器HTTP请求流程
从流程图可以看到,http请求最重要的类就是HttpCache,请求发送前,会先验证是否命中缓存
如果命中,就会发起httpCacheTransaction,去读取缓存信息。当然在读取前会判断是否要进行缓存有效性的验证。如果有变化就更新缓存
未命中,就会发起httpNetworkTransaction去服务端获取,然后根据响应头去判断是否要缓存,以及何种形式的缓存。如果缓存就写入磁盘。
谷歌浏览器的缓存模式有13种,具体可参考http_cache_transaction.cc。
常见的有三种:无缓存,强制缓存,协商缓存
三. HTTP缓存指令
Header信息分为request header和response header。
请求缓存指令:会影响请求发送时,是否要读缓存。
响应缓存指令:会影响接受到响应时,是否要写缓存。
Pragma,Expires是HTTP1.0的字段,在HTTP1.1中已经被Cache-Control取代,不做讨论。
3.1,请求缓存指令
从谷歌代码中可以看到(注意注释),Cache-Control相关的有两个值:
no-cache:如果请求包含这些请求头中的一个,那么避免重用我们的缓存副本(如果有的话)
max-age=0:如果请求包含这些请求头中的一个,则强制缓存副本(如果有)将在重用前重新验证
3.2,响应缓存指令
Cache-control:nostore
响应内容不会写入disk_cache
Cache-control:max-age=3600(1小时)
写入disk_cache,1小时内再次访问该url,强制从缓存中读取。基础时间是第一次访问时,响应中返回的Date
Cache-control:no-cache
再次访问的时候,会去服务端验证
3.3,对于协商缓存,去服务端的验证过程是什么,或者说我们要验证什么。
简单来说就是验证服务器脚本是否改变,改变了就加载新的,分为下面三个步骤:
a,响应头信息是否改变
响应头的改变会影响浏览器对缓存的处理。
比如:响应头由【缓存】->【不缓存】,那么对已有缓存也要根据情况进行相应的删除替换处理
b,资源的最后修改时间是否改变
首次访问的时候,响应会返回Last-Modified,再次访问时,该字段会放在请求中的If-Modified-Since,然后在服务端比较文件的最后修改时间,以确认资源是否发生变化。
这只是时间维度的比较,但如果内容没变,只是时间改变了呢?
为了解决内容对比问题,就有了下面的Etag
c,内容是否改变
首次访问的时候,响应同时会返回Etag,再次访问时,该字段会放在请求中的If-None-Match,然后在服务端比较是否一致。
Etag真的能解决文件内容一致性的问题么?
3.4 ,Etag
Etag 是URL的Entity Tag,用于标示URL对象是否改变。标准概念网上资料很多,这里不做累述。
根据Etag的定义,我们期待的是:当文件内容改变的时候,浏览器能根据Etag去加载最新的资源文件。
是不是真的能达到期望的效果,完全取决于中间件对Etag的算法实现。
我们看看Nginx的Etag生成代码:
ngx_int_t
ngx_http_set_etag(ngx_http_request_t *r)
{
ngx_table_elt_t *etag;
ngx_http_core_loc_conf_t *clcf;
clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
if (!clcf->etag) {
return NGX_OK;
}
etag = ngx_list_push(&r->headers_out.headers);
if (etag == NULL) {
return NGX_ERROR;
}
etag->hash = 1;
ngx_str_set(&etag->key, "ETag");
etag->value.data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN + NGX_TIME_T_LEN + 3);
if (etag->value.data == NULL) {
etag->hash = 0;
return NGX_ERROR;
}
etag->value.len = ngx_sprintf(etag->value.data, "\"%xT-%xO\"",
r->headers_out.last_modified_time,
r->headers_out.content_length_n)
- etag->value.data;
r->headers_out.etag = etag;
return NGX_OK;
}
void
ngx_http_weak_etag(ngx_http_request_t *r)
{
size_t len;
u_char *p;
ngx_table_elt_t *etag;
etag = r->headers_out.etag;
if (etag == NULL) {
return;
}
if (etag->value.len > 2
&& etag->value.data[0] == 'W'
&& etag->value.data[1] == '/')
{
return;
}
if (etag->value.len < 1 || etag->value.data[0] != '"') {
r->headers_out.etag->hash = 0;
r->headers_out.etag = NULL;
return;
}
p = ngx_pnalloc(r->pool, etag->value.len + 2);
if (p == NULL) {
r->headers_out.etag->hash = 0;
r->headers_out.etag = NULL;
return;
}
len = ngx_sprintf(p, "W/%V", &etag->value) - p;
etag->value.data = p;
etag->value.len = len;
}
通过上面的代码我们可以看到nginx的算法:
强Etag=最后修改时间+”-”+内容长度 (16进制)
弱Etag=“\W”+ 强Etag
当我们发布新文件的时候,最后修改时间都会发生变化。基本能满足项目需要。
但通过该算法,我们也能看到,当内容没变最后修改时间变了,就不会命中缓存的问题依然没有得到解决。仅仅判断内容长度还是不够的。
所以对于有能力扩展中间件的公司,都会自行实现Etag算法,目前看来基于文件MD5码是个不错的选择
四,写在最后
通常在项目中,多数都是对响应指令进行控制。
缓存机制完全是依赖浏览器和中间件的实现,本文的分析是基于谷歌和nginx,不代表所有浏览器和中间件完全一致。切记!