这篇文章主要简单介绍浏览器缓存的机制,我们在做Server端响应时间优化的时候经常会接触到缓存的概念,例如经常会用到Memcached或Redis。我曾经使用缓存将原本一分钟才能获取的数据优化到了不到一秒的时间,当然,缓存也不是万能的,某些数据可以使用缓存或者必须使用缓存,有些数据则必须禁用掉缓存,确保每次请求返回的都是最新的数据,技术的运用方式是建立在业务的需求上的。
浏览器有一套自己的缓存机制,而在这个机制的基础上缓存规则的制定则主要依靠后端来实现,这属于一个合格的WEB开发者必须了解的内容。下文中提到的缓存概念,如果没有特殊说明都是指浏览器缓存,本文主要使用Chrome作为客户端,同时对Chrome开发者工具的一些细节进行了介绍。
什么是缓存?
引用Wikipedia上对Web cache的介绍,缓存就是:
A web cache is a mechanism for the temporary storage (caching) of web documents, such as HTML pages and images, to reduce bandwidth usage, server load, and perceived lag.
翻一下大意是:
WEB缓存就是临时存储WEB文档,例如HTML页面、图片等降低带宽使用、服务器负载、提升响应速度的一种机制。
我们使用的任何一个现代浏览器都支持缓存机制。
缓存在哪里?
IE浏览器:
我们可以通过IE的设置界面来查看缓存存放的位置:Internet选项->点击常规选项卡设置按钮->点击查看文件按钮即可自动打开缓存目录。如下图:
enter image description here
Firfox
查看Firefox浏览器缓存可以在Firefox地址栏输入about:cache来查看缓存详情,如图:
enter image description here
Chrome
Chrome并没有直接说明缓存目录、大小等具体信息,不过我们可以用Google查到,以Windows平台为例,Chrome的缓存目录在C:\Users\{{user name}}\AppData\Local\Google\Chrome\User Data\Default\Cache目录下。
Chrome Dev Tools NetWork简单介绍
首先我们对Chrome Dev Tools的NetWork功能做下简单的介绍。
当我们的浏览器向服务器发送请求时会携带一些基本的信息,如User-Agent、Cookie等,我们可以使用浏览器自带的开发者工具看到,如下:
// request header
GET /jquery.autocompleteV2.js HTTP/1.1
Host: 192.168.33.10
Connection: keep-alive
Cache-Control: max-age=0
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36
DNT: 1
Referer: http://192.168.33.10/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-TW;q=0.2
Cookie: cmTPSet=Y; ylCookie=ylCookie; tma=86847064.57393759.1402022771631.1402022771631.1402022771631.1; tmd=6.86847064.57393759.1402022771631.
服务器根据请求返回响应(response),响应头信息,如下:
// response header
HTTP/1.1 200 OK
Server: nginx/1.1.19
Date: Sat, 07 Jun 2014 13:32:29 GMT
Content-Type: application/x-javascript
Last-Modified: Sat, 07 Jun 2014 07:21:44 GMT
Transfer-Encoding: chunked
Connection: keep-alive
Content-Encoding: gzip
下图是Chrome Dev Tools下的网络请求的概览:
chrome_http_request_list
网络概览列表的Size Content栏,上边的数值Size表示响应大小,Size大小 = Content大小 + 响应头大小, Content表示响应内容实际大小,比如浏览器请求了一个实际尺寸为61093字节的文件,那么这里Content的值便为61093byte ÷ 1024 ≈ 59.7KB,Size值不一定大于Content的值,当我们在开启Gzip压缩的情况下Size的大小很可能小于Content的大小,开启Gzip后,尽管服务器没有直接告诉我们响应内容的Content-Length大小,但是浏览器拿到压缩后的响应内容是可以间接的反向计算出响应内容的原始大小的,还有一种情况是Content使用的是本地缓存,也就是服务器返回304状态码告诉浏览器继续使用缓存中的内容,Size的大小只是响应头的大小,Content大小为从缓存中读取的响应内容的大小。
当我们的光标移动到HTTP请求的timeline上时会显示网络请求时间花费的详细信息,如图:
http_timeline
这里涉及到几个名词,下面解释下Chrome NetWork Timeline状态名词的意义:
Blocking代表了等待当前已存在连接可用状态所花费的时间,相当于我们将请求A放入浏览器请求队列里面,浏览器依次处理队列里面的请求直到开始处理请求A所花费的等待时间。
Proxy代表了连接代理服务器所花费的时间。
DNSLookup代表了DNS查找花费的时间。
Connecting代表了创建连接所花费的时间,包括TCP连接建立、DNS查找、连接代理服务器与创建安全连接(SSL)所花费的时间总和。
Sending代表了发送请求数据所花费的时间。
Waiting代表了等待服务器响应所花费的时间。
Receiving代表了接收服务器响应内容花费的时间。
所以这里的Time Latency一栏Time代表了从我们创建请求到浏览器接收到服务器响应内容这个过程所花费的时间,Latency代表了从我们创建请求到服务器生成响应内容这段时间花费的时间。
缓存是如何工作的
更改Nginx配置,对所有js和css文件设置一套有效期为30天的缓存规则,Nginx站点配置添加代码:
location ~* \.(css|js)$ {
expires 30d;
}
重启Nginx服务使配置生效,清空浏览器缓存,模拟首次访问页面,刷新(请点击浏览器工具栏刷新按钮或使用右键菜单刷新功能刷新)页面。这时响应头会多出两个字段的数据:Cache-Control:和Expires,响应头变为了:
// response header
HTTP/1.1 200 OK
Server: nginx/1.1.19
Date: Sat, 07 Jun 2014 13:47:27 GMT
Content-Type: application/x-javascript
Last-Modified: Sat, 07 Jun 2014 07:21:44 GMT
Transfer-Encoding: chunked
Connection: keep-alive
Expires: Mon, 07 Jul 2014 13:47:27 GMT
Cache-Control: max-age=2592000
Content-Encoding: gzip
Cache-Control: max-age=259200表示缓存将在该页面请求后的259200秒后过期。
Expires: Mon, 07 Jul 2014 13:47:27 GMT表示缓存将在2014年7月7号 13:47:27后过期,注意:使用Expires指定过期时间,如果服务器时间和客户端时间不一致(这个问题很常见,在写这篇博客时,我的Ubuntu服务器时间比标准时间慢了4分钟),这时候通过Expires指定过期时间将达不到我们预期的效果,幸运的是绝大多数浏览器(IE6.0都支持HTTP1.1,还用怕什么?)和HTTP Server都已经支持HTTP/1.1,所以当响应头同时有Expires和Cache-Control时浏览器会优先考虑Cache-Control。
那在缓存期有效的这段时间刷新页面浏览器是如何使用已缓存的内容的呢?好,再次刷新(刷新方式同上)浏览器,服务器返回了304的status code,请求头变为了:
// request header
GET /jquery.autocompleteV2.js HTTP/1.1
Host: 192.168.33.10
Connection: keep-alive
Cache-Control: max-age=0
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36
DNT: 1
Referer: http://192.168.33.10/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-TW;q=0.2
Cookie: cmTPSet=Y; ylCookie=ylCookie; tma=86847064.57393759.1402022771631.1402022771631.1402022771631.1; tmd=6.86847064.57393759.1402022771631.
If-Modified-Since: Sat, 07 Jun 2014 07:21:44 GMT
注意这里多了个If-Modified-Since字段,该值就是第一次响应头返回的Last-Modified字段,当服务器收到浏览器发送来的请求头时根据该字段判断是否让浏览器使用缓存内容,若在Last-Modified指定的时间后文件发生了变化服务器返回的status code变为200,同时会将最新的响应内容发送给浏览器,就像我们首次请求一样。如果在Last-Modified指定的时间后文件没有发生变化,服务器将返回status code变为304告诉浏览器:“你可以继续使用之前缓存的内容”,响应头变为:
// response header
HTTP/1.1 304 Not Modified
Server: nginx/1.1.19
Date: Sat, 07 Jun 2014 14:01:50 GMT
Last-Modified: Sat, 07 Jun 2014 07:21:44 GMT
Connection: keep-alive
Expires: Mon, 07 Jul 2014 14:01:50 GMT
Cache-Control: max-age=2592000
如果这段时间请求的文件内容发生了变化,服务器返回的status code变为200,同时响应内容为请求资源的最新副本,响应头内容变为:
HTTP/1.1 200 OK
Server: nginx/1.1.19
Date: Sat, 07 Jun 2014 14:03:00 GMT
Content-Type: application/x-javascript
Last-Modified: Sat, 07 Jun 2014 14:02:56 GMT
Transfer-Encoding: chunked
Connection: keep-alive
Expires: Mon, 07 Jul 2014 14:03:00 GMT
Cache-Control: max-age=2592000
Content-Encoding: gzip
当超出缓存有效期时,再次刷新页面,浏览器与服务器之间请求的协商过程也会验证请求资源是否被修改过,如果没有被修改缓存还是继续有效的,如果请求资源被修改过服务器将重新将请求资源的最新副本发送给浏览器。
页面刷新方式的区别
前面在演示时我们说到用指定的方式去刷新页面,那不同的刷新方式对于缓存会有什么效果呢?
一般常用有四种方式加载页面:
第一次请求时
首次请求初始化缓存机制,无特殊情况。
强制请求
强制请求一般是按下Ctrl + F5组合键来实现,强制刷新时浏览器会忽略一切缓存机制,告诉服务器“把最新的内容给我”,就像我们第一次打开这个页面一样。
一般刷新
常用的一般刷新方法有按下F5键或者在浏览器右键弹出菜单中刷新页面,这时浏览器与服务器之间将按照之前协商好的缓存协议进行对话。
点击浏览器转到按钮
通过浏览器地址栏转到一个已经打开过的页面浏览器将根据已缓存的请求资源是否已经过期来判断是否和服务器进行对话,如果缓存没有过期浏览器直接从缓存中读取数据作为响应内容,浏览器不会与服务器进行对话,不会发送任何的HTTP请求。
更详细的请参考下面的链接:
What requests do browsers’ “F5” and “Ctrl + F5” refreshes generate?
Behind Refresh Button
如何有效的控制缓存?
前面我们说过,当通过地址栏跳转到一个已经访问过的页面时,如果在之前设置的缓存有效期之内浏览器将不会与服务器进行任何协商直接读取本地缓存内容作为响应内容,那即使我更新了一段css代码,那么老访客看到的可能还是从本地读取的老旧的样式表文件,那怎么样才能让我更新的内容强制让客户端与服务器进行对话确认呢?简单:最常见的办法是在文件路径后面添加指定版本号,例如:
每次对文件的修改都要更新版本号,通过这种方式强制让浏览器与服务器进行对话,解决缓存的负面问题。
缓存的意义
合理的设置客户端端缓存可以最大限度的节省服务器网络流量开销、减轻服务器负载、提升用户体验等。
原文地址>>