这是来自 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 是一个非常简单的优化,所以即使是微小的收益也能证明将其包含在我们的堆栈中是合理的。
我们通过在 awsbox 上启动 Persona 的开发实例,打开 Firebug,并对我们的“关于”页面进行 50 次负载时间测量来测试 etagify 对性能的影响——启用和禁用 etagify。(页面加载时间对于我们的用例来说是一个足够好的指标;您可能更关心到达页眉以上内容呈现的时间,或者第一个内容到达页面的时间,或者第一个广告显示的时间。)
在收集原始数据后,我们进行了一些快速统计以查看 etagify 提高了多少性能。我们假设测量值像 钟形曲线 一样围绕平均值分布,计算了两个数据集的平均值和标准差。
令人惊讶的是,我们发现 **etagify 将负载时间缩短了 9%**,从 1.65 秒(标准差 = 0.19)降至 1.50 秒(标准差 = 0.13)。对于几乎没有工作量来说,这是一个很大的提升。
接下来,我们使用 t 检验 来检查在根本不添加 etagify 的情况下观察到这种改进的可能性。我们的 p 值 小于 0.01,这意味着随机性可能导致这种明显改进的可能性小于 1%。我们可以得出结论,测量的改进具有统计意义。
以下是平均前后数据的图表。
总结
我们认为 etagify 非常有用。即使它不是您当前项目的正确工具,希望我们采取的(1)编写专注于解决手头问题的工具,以及(2)进行足够严格的测量以确保您正在取得进展的方法,能为您提供灵感或思考方向。
本系列的往期文章
这是 共 12 篇关于 Node.js 的系列文章 的第六部分。之前的文章是
- 追踪 Node.js 中的内存泄漏
- 全面加载 Node
- 使用安全的客户端会话构建简单且可扩展的 Node.JS 应用程序
- 前端性能优化:第一部分 – 合并、压缩和缓存
- 构建不会熔化的 Node.JS 服务器
关于 Jared Hirsch
@6a68 在 Persona 上进行黑客攻击,弹奏象牙键盘,鞋子总是沾满沙子。
关于 Robert Nyman [荣誉编辑]
Mozilla Hacks 的技术布道师和编辑。发表演讲并撰写有关 HTML5、JavaScript 和开放网络的博客。Robert 是 HTML5 和开放网络的坚定支持者,自 1999 年以来一直在从事 Web 前端开发工作——在瑞典和纽约市。他还会定期在 http://robertnyman.com 上撰写博客,喜欢旅行和结识新朋友。
8 条评论