页面上静态资源的更新

如同 Steve Souders 在他的著作《高性能网站建设指南》中所说的,从网站性能的角度考虑,我们应该尽可能地让页面上不常更改的资源(有时也叫静态资源)在客户端缓存起来,比如图片、js、css 等,这样用户再次访问包含相同资源的页面时,这些资源就可以从本地缓存中读取,而不用再次从服务器请求,这会让页面更快加载,从而让用户体验更好。

随之而来的一个问题就是,这些不常更改的资源有时也会被更改,比如在某个 js 里可能需要增加一个新功能或修复一个 bug。当这些已经被客户端缓存了的资源有更新时,怎么让客户端加载新的版本呢?

被动更新

大多数情况下,这个问题可以通过在资源的 src 地址后面加个版本号或时间戳来解决,比如某个 js 原来的引用为:

<a href="http://cdn.com/1.js?t=20120501">http://cdn.com/1.js?t=20120501</a>

某一天我们更新了这个 js,则可以将它的引用修改为:

<a href="http://cdn.com/1.js?t=20120617">http://cdn.com/1.js?t=20120617</a>

对客户端浏览器来说,上面引用的是两个不同的地址,浏览器会重新从服务器读取 1.js,并使用这个新的资源。到目前为止一切都很美好。

但不幸的是,我们总会遇到一些特殊情况。比如我们有个 js 可能在很多个页面都有引用,并且这些引用不在同一个模板里,如果要修改时间戳得改很多个文件。或者我们的某个 js 是开放给第三方使用的,当这个 js 有更新时,我们不可能一一去通知这些第三方同步更新……

一个典型的例子是 Google Analytics 的埋点脚本 ga.js。无数站点在自己的页面中引用了这个 js 脚本,这个 js 更新之后要通知这么多站点修改引用时间戳是不可能的。

这时,既然不能修改时间戳,那就只能修改缓存时间了。最极端的情况是完全放弃缓存,用户每次请求时都从服务器读取。这样能保证每次请求获取的都是最新的内容,但给服务器的压力就太大了,而且客户端性能也会因为多的这个请求而受到影响。因此,一般的方案是设置一个不太长但是不为 0 的缓存时间(通过设置 header 中的 max-age),这个时间应该是缓存与更新之间的一个平衡点,既要让大部分请求都能从本地缓存中读取从而减小服务器的压力,又不能让缓存时间太长以便资源有了修改之后客户端也能及时更新。

这个具体的平衡点不同的网站有不同的算法,有些可能只是简单地取一个经验值,有一些则可能有一套复杂的算法。比如 Google Analytics 的 ga.js 的缓存过期时间(max-age)为两个小时(注:刚刚发现好像改为 12 个小时了),twitter 的 widgets.js 的缓存过期时间为 30 分钟,Facebook 的 all.js 的缓存过期时间为 18 分钟零 5 秒(1085 秒)。其中 Facebook 的这个过期时间很有意思,不知道他们是根据什么算法得到 1085 这个值的。

主动更新

传统的资源更新方法到上面就结束了。不过,正如小标题所暗示的,除了上面的时间戳(版本号)以及调整缓存时间之外,还有其他的更新资源的方法。

我们先来回顾一下页面刷新的基本知识,比如我们访问的页面中包含一个 /s/atp.js,这个 js 的缓存过期时间设置得很长,如下图:

页面资源请求

可以看到,对于有本地缓存的资源,通过普通的链接访问或者直接在浏览器地址栏输入 URL 访问,这些资源都会从本地缓存读取而不会从服务器请求,除非用户按下 F5 或 Ctrl + F5 刷新页面。

F5 与 Ctrl + F5 有什么区别呢?按下 F5 时,对那些有本地缓存并且缓存还没有过期的资源,浏览器会发一个包含 IF-Modified-Since 头的请求。服务器收到这个请求之后,如果资源没有更新,则会简单地返回一个 304 消息,浏览器收到 304 消息就知道本地缓存中的资源还是最新版本,于是仍然从缓存中读取。当然,如果服务器上的内容更新了,服务器就会返回新的资源内容,浏览器也会使用新的内容而不是读取老缓存。

而按下 Ctrl + F5 时,浏览器的行为就像页面上的资源都没有本地缓存一样,全部向服务器发起一个普通的请求,服务器则返回对应资源的完整内容。

也就是说,只要用户按下了 F5 键(或者 Ctrl + F5),无论用户本地的缓存是否已经过期,浏览器都会去检查或下载服务器上最新的内容。如果再注意到 JavaScript 中的 location.reload() 方法其实就相当于按下了 F5,那么一种新的主动更新资源的方法就呼之欲出了。

JavaScript 中的 location.reload() 方法的作用为重载当前页面,效果与用户按下 F5 一样。还可以传入一个 true 参数,location.reload(true) 表示强制重载当前页面,效果等同于按下 Ctrl + F5(不过根据我的测试,在 IE 、Firefox 下没有问题,在 Chrome 下加这个参数和不加参数效果似乎一样)。

这就带来了这样一种可能:我们可以将缓存时间设得长一些,再通过某种机制知道哪些资源发生了更新,然后在适当的时候调用 location.reload(true) 来刷新这些资源。

Steve Souders 最近在博客文章《Self Updating Scripts》中就介绍了这样一种方法。以类似 ga.js 的打点 js 为例,这个 js 每次执行时总是要发起一个打点请求的,一般的做法是让这个请求返回一个空 gif 图片,但 Steve Souders 很有创意地想到,可以在 js 中写上自己的版本号,发送打点请求时把这个版本号也带上,同时打点服务器根据这个版本号,返回一段内容(而不是空白图片)告诉这个 js 是不是需要更新。如果需要更新了,这个 js 就在当前页面创建一个 iframe,iframe 中包含了需要更新的资源(比如这个 js),最后在这个 iframe 中执行 location.reload(true)。这样一来,就相当于将那些需要更新的资源专门放到一个页面中,然后在那个页面上按下 Ctrl + F5。

大致流程如下图所示。

自更新打点 js

具体的分析和讨论可见 Steve Souders 的原文:Self Updating Scripts

不过,这种方式也并非完美,总的来说,它有两个主要的缺点:

1、需要对原有 js 做一些修改,还需要服务端的支持(判断是否有新版本);
2、资源更新后,用户第一次访问这些资源时,仍然只能访问到缓存中的老资源,需要第二次访问时才能访问到新版本资源(因为要等待 iframe 刷新)。

总的来说,对那些可以使用时间戳或版本号的资源,继续使用时间戳或版本号仍然是不错的选择。对那些不能使用时间戳或版本号的资源,比如公开给很多页面甚至是第三方使用的脚本,或许可以考虑一下这种新的自动更新方法。

更新(2012-11-23):关于这个主题Franky有一篇更详细深入的文章:http://www.cnblogs.com/_franky/archive/2012/07/05/2577141.html

5 Replies to “页面上静态资源的更新”

  1. 不错的思路。不过还是局限性太大,需要打点这样的再次请求逻辑,实际应用中大部分的js是不具有这样的逻辑的。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s