数字营销 · Web开发 · 基础设施

HTTP协议,浏览器缓存和网站速度优化

在计算机和网络的世界里,小到 CPU,大到 Internet,缓存无处不在。个人认为,缓存策略主要解决两个问题:第一个是解决不同设备 I/O 速度不同导致的资源等待问题;第二个是解决相同资源重复传送造成的资源浪费问题。

例如最常提到的 CPU 三级缓存,就是为了解决 CPU 计算速度快,而读取内存速度慢导致 CPU 等待的问题。而我们上网的过程中,浏览器对已经请求过的资源进行缓存,则属于缓存策略解决的第二个问题。

本文主要分析 HTTP 协议、浏览器缓存和网站速度优化。

HTTP缓存

HTTP 属于应用层协议,由请求报文和响应报文组成,都包括 HTTP 头信息(Header)和主体信息(Body)。

在响应信息中,主体信息就是浏览器最终看到的内容;请求报文中,一般只有 POST 等包含请求主体的请求才会携带 Body。

Web 浏览器一般有两种缓存方式。一种是缓存 Body,通过头部信息对服务器和本地信息进行比对。如果符合某些条件,服务器返回 304 Not Modified,浏览器接收到响应后继续加载之前缓存的内容,整个过程如下图:

HTTP缓存原理

当浏览器第一次请求资源时,服务器发送如下响应:

HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: text/html
Last-Modified: Thu, 04 Mar 2015 01:47:20 GMT

<html>
....
</html>

这个响应头中有一个非常重要的信息:Last-Modified,它告诉浏览器该资源上次修改的时间。

浏览器接收到这个信息后,会将资源和相关信息缓存下来。当第二次请求这个资源时,浏览器会自动增加一条:

If-Modified-Since: Thu, 04 Mar 2015 01:47:20 GMT

服务器接收到请求后,会比对资源的修改时间和这个时间。如果资源在这段时间内没有发生变化,则只返回头部信息,而不再返回 HTML 实体:

HTTP/1.1 304 Not Modified
Connection: keep-alive
Content-Type: text/html
Last-Modified: Thu, 04 Mar 2015 01:47:20 GMT

由于 HTML 的内容一般比较大,这样一来,就可以减少重复的数据传输,提高网站加载速度,同时降低带宽消耗。Last-Modified 的信息只能精确到秒。如果一秒之内文件资源发生了多次更改,浏览器可能无法及时加载到新的内容。

为了解决这个问题,又出现了 Etag 和 If-None-Match。

它们的工作原理和上面类似。Web 服务器通过 Etag 标签发送资源的某种 Hash 值,浏览器接收到之后进行缓存。下次访问时,通过 If-None-Match 把缓存的 Hash 值发送给服务器。如果文件内容发生了变化,前后两次的 Hash 值就不一样,服务器会发送最新的文件内容;否则返回 304 Not Modified。

下面这段代码是利用 Etag 和 Last-Modified 为 PHP 页面实现缓存的例子:

<?php
class Cache304{
public $content;
private $contlen;
private $etag;
private $modified;

//设置过期时间,单位秒
public $expiresenconds;

public function __construct($ex){
(int) $this->expiresenconds=$ex;
$this->etag=isset($_SERVER["HTTP_IF_NONE_MATCH"])?$_SERVER["HTTP_IF_NONE_MATCH"]:false;
$this->modified=isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])?$_SERVER["HTTP_IF_MODIFIED_SINCE"]:false;
ob_start(array($this,'get_content'));
}

public function get_content($html){
$this->content=$html;
$this->contlen=strlen($html);
return $html;
}

public function init(){
register_shutdown_function(array($this,'show'));
}

public function show(){
ob_end_clean();

//Etag验证
if($this->etag == md5($this->content)){
header("HTTP/1.1 304 Not Modified");
header("Vary:etag");
exit();
}else{
//注意发送Etag必须要发送Content-Length标签
header("Etag:".md5($this->content));
header("Content-Length:$this->contlen");
header("Last-Modified:".gmdate("D, d M Y H:i:s")." GMT");
echo $this->content;
exit();
}

//Last-Modified验证
if($this->modified && (strtotime(gmdate("D, d M Y H:i:s")." GMT") - strtotime($this->modified) < $this->expiresenconds)){
header("HTTP/1.1 304 Not Modified");
header("Last-Modified:".$this->modified);
header("Vary:Modified");
exit();
}else{
header("Etag:".md5($this->content));
header("Content-Length:$this->contlen");
header("Last-Modified:".gmdate("D, d M Y H:i:s")." GMT");
echo $this->content;
exit();
}
}
}

$a = new Cache304(3600);
$a->init();

//假设以下是原本要输出的内容
echo "你好<br/>";
echo "今天是2015年3月5日<br/>";
echo "欢迎来到我的网站";

运行这段代码,当第二次访问时,服务器确实会返回 304 Not Modified

当强制刷新再次访问时,会发现服务器返回的状态码又变成了 200。这是因为强制刷新时,浏览器会要求服务器重新验证或获取资源,不再直接使用之前的缓存。

上面说到的缓存,实际上只是在发送 HTML 内容时减少了重复传输。服务器原本应该执行的业务逻辑仍然会继续运行,客户端和服务器之间也依然需要建立连接。

网站加载速度虽然提高了,但是对服务器压力的改善并不明显。

HTTP 还提供了另外一种直接读取本地缓存的方式。在这种方式下,浏览器直接读取本地缓存,而不向服务器发送请求。

如下图:

200 ok from cache

HTTP 提供 Cache-ControlExpires 两个头部字段来控制本地缓存。

Cache-Control 可以通过 max-age 指定缓存的有效时间,Expires 则直接指定一个未来的过期时间。具体的用法可以点击这里

需要注意的是,对于直接读取的本地缓存,浏览器的实际行为还会受到刷新方式和具体缓存策略的影响。普通刷新和强制刷新通常都会触发重新验证或重新请求。

对于一个优秀的网站来说,采用合理的多级缓存是很有必要的。不仅能够缩短网站的打开时间,还可以减少带宽和服务器资源消耗,从而服务更多用户。

扩展阅读

  1. HTTP 缓存
  2. Caching in HTTP

评论0

欢迎分享你的看法,也欢迎补充不同的实践经验。