前端性能优化:动态内容缓存与etagify – Node.JS 节日季,第六部分

这是来自 Mozilla 身份团队的《Node.JS 节日季》系列的第 6 部分(共 12 部分)。今天我们将介绍前端性能优化的第二部分。

您可能知道 Connect 会为静态内容添加 ETags,但不会为动态内容添加。不幸的是,如果您动态生成 i18n 版本的静态页面,这些页面根本不会获得缓存头,除非您添加构建步骤来预先生成所有语言的所有页面。这真是个很麻烦的工作。

介绍 etagify

本文介绍了 etagify,这是一种 Connect 中间件,它通过对传出响应主体进行 md5 哈希来动态生成 ETags,并将哈希值存储在内存中。etagify 允许您跳过构建步骤,比您想象的更能提高性能(我们在测试中测得负载时间提高了 9%),并且非常易于使用。

1. 在启动时注册 etagify

myapp = require('express').createServer();
myapp.use(require('etagify')()); // <--- like this.

2. 在您想要缓存的路由上调用 etagify

app.get('/about', function(req, res) {
  res.etagify();  // <--- like that.
  var body = ejs.render(template, options);
  res.send(body);
});

继续阅读以了解有关 etagify 的更多信息:它的工作原理、何时使用它、何时不使用它以及如何衡量您的结果。

(需要复习 ETags 和 HTTP 缓存?我们整理了一份 速查表 来帮助您快速了解。)

etagify 的工作原理

通过专注于一个具体的用例,etagify 只用不到 100 行代码(包括文档)就能完成任务。让我们看看涵盖基本内容的 15 行代码,省略了 Vary 头部处理边缘情况。

需要考虑两个方面:对传出响应进行哈希处理和缓存哈希值;根据传入的条件 GET 请求检查缓存。

首先,这里是我们如何添加到缓存中的。内联注释。

// simplified etagify.js internals

// start with an empty cache
// example entry:
//   '/about': { md5: 'fa88257b77...' }
var etags = {};

var _end = res.end;
res.end = function(body) {
  var hash = crypto.createHash('md5');

  // if the response has a body, hash it
  if (body) { hash.update(body); }

  // then add the item to the cache
  etags[req.path] = { md5: hash.digest('hex') };

  // back to our regularly-scheduled programming
  _end.apply(res, arguments);
}

接下来,我们来看看如何检查缓存。同样,内联注释。

// the etagify middleware
return function(req, res, next) {
  var cached = etags[req.path]['md5'];

  // always add the ETag if we have it
  if (cached) { res.setHeader('ETag', '"' + cached + '"' }

  // if the browser sent a conditional GET,
  if (connect.utils.conditionalGET(req)) {

    // check if the If-None-Match and ETags are equal
    if (!connect.utils.modified(req, res)) {

      // cache hit! browser's version matches cached version.
      // strip out that ETag & bail with a 304 Not Modified.
      res.removeHeader('ETag');
      return connect.utils.notModified(res);
    }
  }
}

何时(以及何时不)使用 etagify

etagify 的方法非常简单,对于在服务器运行期间不会更改的动态生成的页面(如 i18n 静态页面)来说是一个很好的解决方案。但是,etagify 在处理其他常见用例时有一些注意事项。

  • 如果页面在首次缓存后发生更改,用户将始终看到过时的缓存版本。
  • 如果页面针对每个用户进行个性化定制,可能会发生两种情况。
    • 如果使用 Vary:cookie 头部来分别缓存用户的个人页面,则 etagify 的缓存将无限增长。
    • 如果没有 Vary:cookie 头部,则第一个进入缓存的版本将显示给所有用户。

衡量性能改进

我们没有预见到 etagify 会带来巨大的性能提升,因为条件 GET 请求仍然需要进行 HTTP 往返,避免页面重新下载只会为用户节省几 KB(见截图)。但是,etagify 是一个非常简单的优化,所以即使是微小的收益也能证明将其包含在我们的堆栈中是合理的。

firebug screen cap showing 2kb savings

我们通过在 awsbox 上启动 Persona 的开发实例,打开 Firebug,并对我们的“关于”页面进行 50 次负载时间测量来测试 etagify 对性能的影响——启用和禁用 etagify。(页面加载时间对于我们的用例来说是一个足够好的指标;您可能更关心到达页眉以上内容呈现的时间,或者第一个内容到达页面的时间,或者第一个广告显示的时间。)

在收集原始数据后,我们进行了一些快速统计以查看 etagify 提高了多少性能。我们假设测量值像 钟形曲线 一样围绕平均值分布,计算了两个数据集的平均值和标准差。

令人惊讶的是,我们发现 **etagify 将负载时间缩短了 9%**,从 1.65 秒(标准差 = 0.19)降至 1.50 秒(标准差 = 0.13)。对于几乎没有工作量来说,这是一个很大的提升。

接下来,我们使用 t 检验 来检查在根本不添加 etagify 的情况下观察到这种改进的可能性。我们的 p 值 小于 0.01,这意味着随机性可能导致这种明显改进的可能性小于 1%。我们可以得出结论,测量的改进具有统计意义。

以下是平均前后数据的图表。

normal distributions with and without etagify

总结

我们认为 etagify 非常有用。即使它不是您当前项目的正确工具,希望我们采取的(1)编写专注于解决手头问题的工具,以及(2)进行足够严格的测量以确保您正在取得进展的方法,能为您提供灵感或思考方向。

本系列的往期文章

这是 共 12 篇关于 Node.js 的系列文章 的第六部分。之前的文章是

关于 Jared Hirsch

@6a68 在 Persona 上进行黑客攻击,弹奏象牙键盘,鞋子总是沾满沙子。

Jared Hirsch 的更多文章……

关于 Robert Nyman [荣誉编辑]

Mozilla Hacks 的技术布道师和编辑。发表演讲并撰写有关 HTML5、JavaScript 和开放网络的博客。Robert 是 HTML5 和开放网络的坚定支持者,自 1999 年以来一直在从事 Web 前端开发工作——在瑞典和纽约市。他还会定期在 http://robertnyman.com 上撰写博客,喜欢旅行和结识新朋友。

Robert Nyman [荣誉编辑] 的更多文章……


8 条评论

  1. Mikeal Rogers

    我在几个地方都有类似的代码,只是我处理流更困难一些。

    我对这段代码的一个担忧是,etags 对象会无限增长。如果您的服务器有很多资源,这可能是一个问题。这就是我依赖 @izs 的 `lru-cache` 模块按 URL 而不是普通对象来存储 etags 的原因。

    2013 年 2 月 19 日 上午 09:30

    1. Jared Hirsch

      嗨,Mikeal,

      感谢您的评论。

      我完全同意无限缓存会带来麻烦。我尝试在上面的“何时使用以及何时不使用”部分中提请注意这一点。

      特别地,如果您发送 Vary:cookie 头部,则缓存将为每个登录用户添加一个条目,这些用户访问了给定的资源。

      2013 年 2 月 19 日 下午 01:37

  2. Tobin

    请多写点,内容很棒。

    2013 年 2 月 19 日 上午 11:04

    1. Jared Hirsch

      谢谢!两周后我们将发布另一篇文章。

      2013 年 2 月 19 日 下午 01:38

  3. Dan

    从代码片段来看,它似乎没有处理流式响应。

    2013 年 2 月 19 日 下午 01:53

    1. Jared Hirsch

      实际上,etagify 处理流式响应。

      为了使代码示例尽可能简单,我省略了 res.write 逻辑,但是 这里 是源代码中的相关部分。

      2013 年 2 月 19 日 下午 02:01

  4. Allan Ebdrup

    我从 CDN 提供所有静态文件,Node 只有 REST API。当您流式传输数据时,我认为这将不起作用。

    2013 年 2 月 22 日 上午 02:58

  5. Pavel Nikolov

    如果库不依赖于 connect 会很好。(例如,这里返回 connect.utils.notModified(res);或者返回 connect.utils.notModified(res);)

    这样,其他开发人员就可以使用它来例如与 restify 结合使用。所有使用 function(req, res, next) 的中间件都可以在 express 和 restify 中使用。

    2013 年 3 月 17 日 上午 07:44

本文的评论已关闭。