本文最初发布于 Casper 技术博客,经原作者授权由 InfoQ 中文站翻译并分享。
我们在 casper.com 上部署了一个变更,从我们自己的服务器而非供应商的服务器上加载一段第三方 JavaScript 代码。这个改动将初始渲染时间缩短了 1.7 秒:
在桌面 Chrome w/3G 网络下测试的数据
上文说的第三方 JavaScript 来自一家名为 Optimizely 的公司。通过使用他们的客户端 JavaScript,我们在 casper.com 上进行 a/b 测试。一旦 JavaScript 文件下载完成并执行,它将改变网站一半访客的文档,从而度量他们对变化的反应。为确保尽可能地避免文档样式短暂失效(FOUC),我们遵循的最佳实践是以阻塞方式最先加载 Optimizely。
正如我们所预期,采用这种方式加载 JavaScript 代码段会对我们网站的 Web 性能产生负面影响。
为此,我们在很长一段时间进行权衡。
我们是应该遵循 Web 性能最佳实践并异步加载 Optimizely JavaScript,还是遵循最佳实践的实验以阻塞方式加载?无论哪种,各有利弊。
我们想到的一个好方法是自托管 Optimizely 代码段。一般,像 Optimizely 这样的供应商会提供一个 JavaScript 文件的 URL(由他们托管)。问题是,这会导致新的 DNS 查询以及与供应商服务器之间新的 HTTP 连接和 SSL 握手。
用这种方式加载的另一项成本是,无法使用HTTP2多路复用来提供资产,而对于浏览器和服务器来说,这是一种更有效的通信方式。如下面的截图所示,从我们一项性能测试中可以看出,这会导致 DNS 查询延迟 39ms,建立服务器连接延迟 54ms,SSL 握手延迟 135ms。
此外,在等待第一个字节时有 175ms 的延迟,如果我们能使用 HTTP2 多路复用,就可以消除这一延迟。
自托管文件的最后一个好处是,我们能更好地控制边缘(CDN)和客户端(浏览器)缓存。Optimizely 不会让你控制它们的边缘缓存,但它们可以让你控制客户端缓存。有一个设置允许你配置 cache-control 值,我们将其设置为 2 分钟。对我们而言,当文件由 Optimizely 托管时,这是一个理想设置。
为了证明自托管更好,我们手动复制了 Optimizely JavaScript 文件的内容,并在服务器上保存了一个版本,同时,替换 staging 环境中的引用,指向我们的自托管版本。结果并不明显。这让人相当失望,以至于我们的数据分析师说,为了在初始渲染时间上节省 200 毫秒,不值得做这么多事。对这个说法,我们一致同意。
我们一直在坚持,因为我们认为 staging 环境不是很适合测试这种性能变化。我们的 staging 环境缺少很多只在生产环境中运行的第三方 JavaScript。所以我们设计了一个产品测试,在这个测试中,我们部署 Optimizely 的静态自托管版本,而数据分析师在 3 天内不对 Optimizely 做任何更改。
下降的时段是在 Optimizely 的自托管版本部署到生产环境期间(测量环境为桌面 Chrome、有线网络连接——当时我们没有在 3G 网络的速度下测量性能,这就是为什么文章一开始的图里效果更明显,但现在 3G 是我们测量时的标准网速)
从上图可以看到,当 Optimizely 代码段的自托管静态版本在生产环境中运行时,初始渲染时间出现下降。通过自托管,由于我们消除了 DNS 查询、Optimizely 服务器连接、SSL 握手、首字节时间,并启用了 H2 多路复用,所以初始渲染时间大大减少。
不过,我们还没有做好永久改变的准备。Optimizely 的工作方式是,如果对实验做了更改,JavaScript 代码段会在 Optimizely 服务器上更新。更改可能是开始/暂停一个实验,修改一个实验等等。你所做的任何更改都会生成新版本的 JavaScript 文件。
因为只是在生产环境中加载了手动复制的 JavaScript 文件的静态副本,所以我们不能一直保存它,因为那样我们就永远无法开始/暂停实验。对软件工程师来说,每次更改时都要手动复制新文件,这相当麻烦。
所以,既然我们看到这种方法的好处,就必须弄清楚如何从我们自己的服务器动态加载最新版本的 Optimizely 代码段。
为此,我们创建了一个每 60 秒运行一次的 AWS Lambda。当运行时,它会向 optimizely.com 发送一个 JavaScript 文件请求。它创建文件的散列,并检查 S3 以确定散列是否变化(我们将上次执行时的散列存储在 S3 上的一个文件里)。如果散列发生变化,则将新的 JavaScript 文件保存到 S3,文件名中包含散列的一部分(例如:snippet-c36d504bc3c26479f1181e6119617a64.js)。接下来,Lambda 将散列发送给我们 Fastly 边缘服务器上的一个字典。这就是奇妙之处。我们将边缘服务器配置为 ESI(Edge Side Include)和边缘字典的组合,动态地将最新的 Optimizely JavaScript 文件名插入到边缘服务器提供的每个页面的 HTML 中。这让我们可以在边缘处更新对 Optimizely 文件的引用,而不必每次文件更改时都重新部署网站。
以下是 WebPageTest 的截图,它测量的是由 Casper 托管的新 Optimizely 文件的性能:
下面是通过 WebPageTest 收集到的自托管前后的数据对比:
理想情况下,对于这些值,我们会提供实际用户监控(RUM)的 95 百分位数据,但对于 casper.com,我们还没有完全实现这一点。据推测,Optimizely 托管的时间(我们不确定是好是坏)和 Casper 托管内容的下载时间会有一些波动,因为这是合成测试。
这是一个 Chrome 瀑布流,显示了在 casper.com 和 Optimizely 文件上运用 HTTP2 多路复用的效果。请注意,前 5 个资产的内容下载几乎是在同一时间开始的。
最后,如前所述,自托管让我们能更好地控制缓存。我们将边缘服务器配置为将文件在边缘和浏览器缓存中保存整整一年。之所以能这样做,是因为文件名对于内容是惟一的(我们将文件散列的一部分添加到文件名中),并在内容更改时替换对文件名的引用。这样,如果我们不对 Optimizely 代码片段做任何修改,重复访客的浏览器甚至不会向 casper.com 请求文件。相反,它将直接从用户文件系统的缓存中提取文件。超级快!
如下图所示,你可以看到从浏览器缓存提供文件的好处:
这种方法的缺点是,当我们频繁地修改 Optimizely 的代码段时,网站访问者将无法体验到最优缓存。随着业务增长,我们的数据分析师可能会运行更多的 a/b 测试,这就需要频繁地修改文件。这可能导致网站访问者在访问 casper.com 期间需要下载多个版本的文件。我们在一个自定义的 DataDog 仪表盘跟踪 JavaScript 文件的每次修改:
在这个图表中,我们可以看到,在 23 日 3 个小时的时间里,代码段更改了大约 25 次。由于我们的平均访问持续时间不是很长,因此,不太可能有大量的访问者以这种更改频率下载代码段的多个版本。
总的来说,我们认为自托管的好处多于缺点。
我们的软件工程师、产品经理、站点可靠性工程师和数据分析师经过大约一个月断断续续的工作完成了这个项目。
英文原文:
How we shaved 1.7 seconds off casper.com by self-hosting Optimizely
评论