前端性能优化:Add Expires headers
Expires headers 是什么?
Expires headers:直接翻译是过期头。Expires headers 告诉浏览器是否应该从服务器请求一个特定的文件或者是否应该从浏览器的缓存抓住它。Expires headers 的设计目的是希望使用缓存来减少HTTP requests的数量,从而减少HTTP相应的大小。
Expires headers 中的 Expires 说明了 Expires headers 是有时间限制的,只有在这个指定的时间期限内,浏览器才会从缓存读取数据,而超过这个时间期限,再次访问同一个页面时浏览器还是会向服务器发起 HTTP requests,从服务器端下载页面所需的文件。
Max-age 和 mod_expires
(注释:本小节内容节选至Steve Souders的《High Performance Web Sites》)
在介绍如何很好地改善传输性能之前,需要提及除了 Expires 头之外另一种选择。 HTTP 1.1 引入了 Cache-Control头来客服Expires头的限制。因为 Expires 头使用一个特定的时间,它要求服务器和客户端的时钟严格同步。另外,过期日期需要经常检查,并且一旦未来这一天到来了,还要在服务器中提供一个新的日期。
换一种方式,Cache-Control 使用 max-age 指令制定组件被缓存多久。它以秒为单位定义了一个更新窗。如果组件被请求开始过去的时间少于max-age,浏览器就使用缓存版本,这就避免了额外的HTTP请求。一个长久的max-age头可以刷新窗为未来10年。
Cache-Control: max-age=315360000
使用带有max-age的Cache-Control可以消除Expires头的限制,但对于不支持HTTP 1.1的浏览器(尽管这只占你的访问量的1%以内),你可能仍然希望体统Expires头,你可以同时指定两个响应头——Expires和Cache-Control max-age。如果两者同时出现,HTTP 规范规定max-age指令将重写Expires头。所以,你会看到在 PageSpeed 的前端性能优化规则 Leverage browser caching 看到这样的说明:
It is important to specify one of Expires or Cache-Control max-age, and one of Last-Modified or ETag, for all cacheable resources. It is redundant to specify both Expires and Cache-Control: max-age, or to specify both Last-Modified and ETag.
Google 的工程师认为同时设置 Expires 和 Cache-Control: max-age 是一种冗余,但是实际的开发中,为了前面提到的不支持HTTP 1.1的不到1%的用户,我们通常还是同时设置了这个两个头信息。
另外, PageSpeed 文档对于HTTP 1.1 提供的相应的缓存头信息做了更具体的介绍:
- Expires and Cache-Control: max-age. These specify the “freshness lifetime” of a resource, that is, the time period during which the browser can use the cached resource without checking to see if a new version is available from the web server. They are “strong caching headers” that apply unconditionally; that is, once they’re set and the resource is downloaded, the browser will not issue any GET requests for the resource until the expiry date or maximum age is reached.
- Last-Modified and ETag. These specify some characteristic about the resource that the browser checks to determine if the files are the same. In the Last-Modified header, this is always a date. In the ETag header, this can be any value that uniquely identifies a resource (file versions or content hashes are typical). Last-Modified is a “weak” caching header in that the browser applies a heuristic to determine whether to fetch the item from cache or not. (The heuristics are different among different browsers.) However, these headers allow the browser to efficiently update its cached resources by issuing conditional GET requests when the user explicitly reloads the page. Conditional GETs don’t return the full response unless the resource has changed at the server, and thus have lower latency than full GETs.
Expires 和 Cache-Control: max-age. 制定缓存文件的 “freshness lifetime”(这里我就翻译为过期时间)。这个前面我已经介绍过了,只是 “the browser will not issue any GET requests for the resource until the expiry date or maximum age is reached.”,更准确的说应该是说浏览器还是会向服务器发送一个GET请求,只是服务器发现请求的文件是一个设置了 Expires header 的文件,就只返回一个304的状态信息,而不是发送一个完整的文件到浏览器,接受到304状态码后,浏览器就会请求本地已经缓存的静态文件了。
Last-Modified and ETag的说明在介绍另外的 YSlow 的规则 Configure entity tags (ETags) 会给大家做详细的说明。这里简单的说明一下,这两个是头服务器在检测缓存的组件是否和原始服务器上的组件匹配时有两种方式。
服务器会通过 Last-Modified 相应头来返回组件的最新修改日期,而浏览器会使用 If-Modified-Since 头将最新修改日期传回到服务器经行比较。如果比配,就会返回 304状态码,而不是重新下载组件。ETag 提供了另外一种方式,浏览器通过 If-None-Match 头发送本地资源的 ETag值,跟服务器端返回的ETag值对比,一致则返回304状态,浏览器读取本地资源。ETag 值的优先级比 Last-Modified 高,如果服务器端的 Last-Modified 和本地的 If-Modified-Since 值相同,但是服务器和本地的 ETag 值和 If-None-Match 不同,浏览器而就会重新下载请求的资源。
为什么要 Add Expires headers?
我们知道,当你访问一个网站,你的浏览器负责从服务器下载所需的所有文件。这里的下载就是我们前面介绍过的HTTP requests。由于HTTP协议是无状态协议,所以如果不加任何处理的话,浏览器在访问同一个页面时是会反复向服务器请求相同文件的,这样会给服务器带来不必要的下载负担。而随着网页内容变得越来越丰富,所以页面的 HTTP requests 也越来越多。设置 Expires headers,让浏览器从本地读取已经下载过的缓存文件就会减少很多 HTTP requests 了。当然,页面的加载速度就会快很多。
这里有两点需要明确:
- Add Expires headers 能够减少 HTTP requests,是指的在浏览器再次访问同一个页面(或者再次请求同一个文件)时,浏览器才会从本地读取缓存文件。而用户第一次访问页面时,Expires headers是不起作用的。所以才有《前端性能优化:Make fewer HTTP requests》中介绍的,在首次加载Web页面时的 HTTP requests 通常会比再次访问时的 HTTP requests 多。
- Expires headers 是有时间限制的,超过了指定的过期时间,浏览器会再次想服务器发出 HTTP requests,而不是读取本地的缓存文件。
Add Expires headers 的规则
在了解了 Expires headers 相关的基础知识后,我们现在要介绍的是 Add Expires headers 的规则,还是看看YSlow的文档吧:
There are two aspects to this rule:
- For static components: implement “Never expire” policy by setting far future
Expires
header- For dynamic components: use an appropriate
Cache-Control
header to help the browser with conditional requests
我们看到,有两点规则:
- 静态文件:落实“永不过期”的原则,通过设置一个足够长的过期时间来实现。
- 动态文件:设置一个适当的
Cache-Control
头。怎么才算适当?原则很简单,根据这个动态内容的变更频率做出设置,经常修改的,就设置较短的过期时间,不怎么变动的就设置长一些的过期时间。
另外要说明一点,YSlow 建议静态文件最短的过期时间为6天。 而 PageSpeed 则认为要设置得更就一些,至少30天。
Set Expires to a minimum of one month, and preferably up to one year.
最后要注意的就是添加了(长时间的) Expires headers 的文件,如果在设置的过期时间内需要修改时,你必须修改文件名或者添加修改文件的时间戳。这样用户再访问页面时,才会向服务器请求新文件。例如你需要修改首页的layout.css文件,你可以这么处理:
// 原来的调用处理
<link href="/css/layout.css" rel="stylesheet" />
// 调用新文件的处理
<link href="/css/layout-v20140913.css" rel="stylesheet" />
// 或者这么处理
<link href="/css/layout.css?version=v20140913" rel="stylesheet" />
如何 Add Expires headers?
在介绍具体的服务器端的设置时,其实你需要先确定你要给哪些(这里主要是针对静态文件)文件设置 Expires headers。通常需要缓存的文件有这些:
- Images: jpg, gif, png
- favicon/ico
- JavaScript: js
- CSS: css
- Flash: swf
- PDF: pdf
- media files:视频,音频文件
HTML 文件不要设置 Expires headers。实际的开发经验告诉我,给HTML文件添加 Expires headers 会带来很多的麻烦。即便你要添加 Expires headers,也尽量设置较短的过期时间。这一点在 PageSpeed 的 Leverage browser caching 规则中也明确提到了:
In general, HTML is not static, and shouldn’t be considered cacheable.
知道要缓存哪些资源文件后,接着就是预判这些文件的变更频率,设置合适的过期时间。还是前面的原则,变更频繁的 Expire 时间就越短,不怎么变动的就可以设置长的过期时间,也就是落实“永不过期”的原则。
Apache 服务器配置 Expires headers
接下来就是在服务器端设置 Expires headers 了,这里以Apache服务器为例,我们在 .htaccess 文件中配置(.htaccess是跟目录下的一个隐藏文件)添加如下代码:
<IfModule mod_expires.c>
# Enable expirations
# 开启 Expires headers
ExpiresActive On
# Default directive
# 默认的过期时间
ExpiresDefault "access plus 1 month"
Cache-Control max-age=2592000
# My favicon
# 针对 ICON 文件的配置
ExpiresByType image/x-icon "access plus 1 year"
# Images
# 针对图片的配置
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/jpg "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
# CSS
# 针对 CSS 文件的配置
ExpiresByType text/css "access 1 month"
# Javascript
# 针对 JavaScript 文件的配置
ExpiresByType application/javascript "access plus 1 year"
</IfModule>
Ngnix 服务器配置 Expires headers
现实的情况是现在的在服务器端网站的静态文件通常都是通过 Ngnix 服务器处理的。然后通过Ngnix配置反向带代理指向Apache服务器处理动态内容。在Ngnix 服务器的配置代码如下:
# 以下代码加入到ngnix服务器的server区块中
server {
# cache static files
location ~* \.(gif|jpe?g|png|ico|swf)$ {
# d - 天
# h - 小时
# m - 分钟
expires 168h;
add_header Pragma public;
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
}
# 由于js和css文件需要改动,设置的时间为5分钟
location ~* \.(css|js)$ {
expires 5m;
add_header Pragma public;
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
}
}
这里还要另外补充一下 PageSpeed 文档中提及的设置缓存的一些技巧:
- Use fingerprinting to dynamically enable caching.
- For resources that change occasionally, you can have the browser cache the resource until it changes on the server, at which point the server tells the browser that a new version is available. You accomplish this by embedding a fingerprint of the resource in its URL (i.e. the file path). When the resource changes, so does its fingerprint, and in turn, so does its URL. As soon as the URL changes, the browser is forced to re-fetch the resource. Fingerprinting allows you to set expiry dates long into the future even for resources that change more frequently than that. Of course, this technique requires that all of the pages that reference the resource know about the fingerprinted URL, which may or may not be feasible, depending on how your pages are coded.
- Set the Vary header correctly for Internet Explorer.
- Internet Explorer does not cache any resources that are served with the Vary header and any fields but Accept-Encoding and User-Agent. To ensure these resources are cached by IE, make sure to strip out any other fields from the Vary header, or remove the Vary header altogether if possible.
- Avoid URLs that cause cache collisions in Firefox.
- The Firefox disk cache hash functions can generate collisions for URLs that differ only slightly, namely only on 8-character boundaries. When resources hash to the same key, only one of the resources is persisted to disk cache; the remaining resources with the same key have to be re-fetched across browser restarts. Thus, if you are using fingerprinting or are otherwise programmatically generating file URLs, to maximize cache hit rate, avoid the Firefox hash collision issue by ensuring that your application generates URLs that differ on more than 8-character boundaries.
- Use the Cache control: public directive to enable HTTPS caching for Firefox.
- Some versions of Firefox require that the Cache control: public header to be set in order for resources sent over SSL to be cached on disk, even if the other caching headers are explicitly set. Although this header is normally used to enable caching by proxy servers (as described below), proxies cannot cache any content sent over HTTPS, so it is always safe to set this header for HTTPS resources.
- Use fingerprinting to dynamically enable caching:fingerprinting 处理是针对偶尔会修改的文件,但不确定什么时候修改的时候采取的处理措施。其实这个处理技巧我觉得最简单的应用就是我前面提到的时间戳的处理技巧。你可以给这个文件设计较长的过期时间,但是你却可以比较频繁的修改,一旦版本确定后,长的过期时间还是会发挥作用。
- Set the Vary header correctly for Internet Explorer:针对IE浏览器设置 Vary 头。IE 浏览器不会缓存被送达 Vary 头和任何领域的任何资源,但接受Accept-Encoding 和 User-Agent。所以为了确保IE浏览能够正确缓存资源,应该去掉 Vary 头信息中的其他信息,如果可以干脆就清空Vary 头信息。不过服务器端我们通常设置 Vary:Accept-Encoding,并且是针对文本类型的资源文件。
- Avoid URLs that cause cache collisions in Firefox:简单的讲,这是针对Firefox对相同URL地址文件只缓存其中一个要注意的问题。在使用fingerprint类似技巧自动命名文件名的时候,生成的文件名一定要超过8个字符长度,避免Firefox重名文件产生相同的hash key。(不过这种情况,我实际开发式还没有遇到过,不过大家可以参考一下 Remove duplicate JavaScript and CSS 规则中介绍的关于重复调用资源的问题。)
-
Use the Cache control: public directive to enable HTTPS caching for Firefox:也是针对Firefox的设置。在 Cache-Control头中添加 public值可以确保Firefox缓存HTTPS协议中请求的资源。实际上在我上面的 Ngnix 服务器中配置时就设置了public值:
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
空缓存VS完整缓存
湖北省*门户网站首页的空缓存网络请求视图
湖北省*门户网站首页的完整缓存网络请求视图
看看我现在工作的湖北省*门户网站首页的空缓存和完整缓存的网络请求视图视图大家应该是一目了然了。空缓存时页面请求的大小是1.9M,46个请求,完成页面加载需要43.73秒,而完整缓存请求的大小是214.2K,只有4个新请求,页面加载时间才526毫秒。不用过多解释了,设置 Expires headers 带来的性能优化的效果