解决单页应用程序的 OPTIONS 性能问题

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

[本文与 Matt Aimonetti 合着]

如今,单页应用程序风靡一时。 各种 新的 JavaScript 框架 使得用 JavaScript 编写复杂的应用程序变得非常容易,大多数 MVC 模式都在浏览器中运行。当然,这也有其自身的挑战,而在 SOASTA,我们始终准备好应对这些挑战。

早在 5 月份,我的 boomerang 开发伙伴 Nic Jansma 就解释了我们如何破解 boomerang 来衡量单页应用程序的性能 。今天我们来说说另一个问题: CORS 发挥作用时SPA 的性能。

Splice 的联合创始人、科技企业家和定期会议发言人 Matt Aimonetti 最近遇到了这个问题,并同意共同撰写这篇文章,分享他的经验。

但首先,一些历史......

早在 1990 年代后期,Microsoft 就引入了 XMLHttpRequest 对象,以便在不中断页面流的情况下从 JavaScript 发出后台 HTTP 请求;然而,因为没有其他浏览器(又名 Netscape)支持它,所以它基本上没有引起注意。

2004 年 4 月 1 日,作为精心制作的愚人节玩笑的一部分,谷歌发布了被许多人认为是第一个广为人知的单页应用程序(我们当时称之为 Rich Internet Applications)。他们称之为 Gmail,各地的网络开发人员开始查看代码以了解他们是如何做到的。

2005 年,杰西·詹姆斯·加勒特 (Jesse James Garrett) 创造了术语 AJAX 来描述这些应用程序使用的通信方法,各地的标准制定者决定创建最佳实践,以避免掉入无法维护的代码的兔子洞,而这些代码无法满足我们努力创建的可访问性标准.或者正如 Thomas Vander Waal 所说:

“它必须很好地降解。它必须仍然可以访问。它必须可用。否则,对于某些或许多人来说,它就是一个很酷的无用垃圾。”

就此,Jeremy Keith 向我们 介绍了 Hijax

现在早些时候,浏览器开发人员意识到他们不能只允许您在任何地方进行 XHR 调用,因为这将允许攻击者依赖用户登录的 cookie 在后台窃取第三方信息,因此 XHR 仅限于相同的 -原始策略,即,您只能对与您进行调用的页面位于同一域中的服务器进行 XHR 调用。您可以更改 document.domain 以使此域检查的限制稍微少一些,但它仍然限于当前页面的父域。

输入 CORS

这种安全模型有点奏效,但我们也进入了 Web API 时代,第三方希望随机网站能够从他们的服务器中提取数据,可能会使用用户登录的 cookie 来获取个性化信息。但更重要的是,网站希望能够使用他们自己的 API,这些 API 可能来自不同的域(例如他们的 CDN)。 动态脚本节点 和 JSON-P 之类的东西起作用了,但它们破坏了安全模型,使得保护这些服务免受跨站点请求伪造的影响变得更加困难。

Web 标准组介入并引入了跨源资源共享或 CORS 的概念,它指出服务器可以通过 Access-Control-Allow-Origin 标头指定允许与哪些源共享其内容。

预检请求

不幸的是,每一个很酷的规范也伴随着意想不到的安全考虑。对于 CORS,这是预检请求。

根据 MDN

特别是,如果出现以下情况,请求将被预检:

  • 它使用 GET、HEAD 或 POST 以外 的方法。此外,如果 POST 用于发送 Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain 的请求数据,例如,如果 POST 请求向服务器发送 XML 负载使用 application/xml 或 text/xml,然后对请求 进行 预检。
  • 它在请求中设置自定义标头(例如,请求使用 X-PINGOTHER 等标头)

这个想法是向服务器请求发送自定义标头的权限,正如 其他人 发现的 那样,常用的 X-Requested-With HTTP 标头往往会触发此操作。

Splice ,我们有一个 Go 后端,我们从一个与我们的 API 对话的 Rails 前端开始。随着时间的推移,JQuery 代码量开始变得难以维护,Rails 渲染逐渐成为瓶颈。我们将前端从 Rails 移植到 Angular,一切似乎都很好……直到我们开始听到非美国用户的抱怨,说网站的某些部分对他们来说速度很慢。事实证明,这些是我们进行许多 API 调用以访问用户特定/签名/加密资源的部分。通过 VPN 连接检查网络调用,我们注意到主要问题是延迟。

瑞典和加利福尼亚之间的延迟不是很好,但更糟糕的是每个 API 调用都必须等待预检 OPTIONS 调用,然后才能发送实际请求。我们的 API 响应时间很快(低于 10 毫秒),但一些用户会看到响应时间超过 500 毫秒!

这第二个 HTTP OPTIONS 请求使获取数据的延迟加倍……而 Real Users™ 讨厌延迟。

那么,我们如何摆脱这个额外的请求呢?

X-请求-With

为此,我们需要了解开发人员为何使用 X-Requested-With 标头,这将我们带回到 2005 年所有那些围绕 Ajax 的最佳实践。为了对所有资源使用规范的 URI,我们对整页请求和 XHR 请求使用相同的 URL,并使用 X-Requested-With 标头来区分请求者。

但是,这有几个问题:

  1. X-Requested-With 是一个自定义标头,希望成为事实上的标准
  2. 我们根据请求者的类型而不是其宣传的功能或请求的响应格式为同一 URL 发送不同的响应。

这开始闻起来很像 90 年代后期,当时我们为 Internet Explorer 和 Netscape 提供不同的 HTML。

这些问题的解决方案是使用标准标头来正确宣传功能或请求的响应格式,事实证明 HTTP 规范 确实有一个标准标头用于此目的。

其他自定义标头

在最近版本的 Angular 和 JQuery 中,除非明确添加,否则这个标头实际上默认被删除,所以这对 Splice 来说不是问题,但是因为我们过去常常发送这个标头(通过 JQuery),所以我们错误地认为预检请求是不可避免的.

但是,我们确实有其他自定义标头,用于报告发出请求的 JavaScript 代码的版本(git hash)。这些标头具有相同的效果,因为它们未能通过预检的自定义标头检查。

接受标头

RFC 2616 第 14.1 节中描述的 Accept 标头允许调用者指定它愿意接收的内容类型。默认情况下,浏览器会将 text/html 作为 首选 可接受类型。在发出 XHR 请求时,我们可以指定 application/json、application/xml、text/csv 或任何其他内容作为 唯一 可接受的内容类型。

然后,您的服务器应用程序可以查看 Accept 标头,以决定是使用完整的 HTML 响应,还是使用数据的 JSON 表示形式或其他方式进行响应。这方面的技术术语称为 内容协商

其他实现将查询字符串参数添加到 URL,指定请求的内容类型或与客户端库相关的其他参数,如版本或哈希。

缓存

如果这些请求的响应具有适当的缓存控制标头,则可以缓存它们。重要的是要确保服务器使用 Vary 标头指定 Accept 标头用于生成协商的内容内容,因此应该是缓存键的一部分,无论是在浏览器中还是在中间代理中。

概括

  1. 如果服务器支持适当的 CORS 标头,则跨域 XMLHttpRequests 可以工作。
  2. 添加自定义标头或使用非标准内容类型会强制浏览器发出预检 OPTIONS 请求以确定这些是否可接受,这有效地加倍了获取数据的延迟。
  3. 避免自定义 HTTP 标头,而是使用标准标头(如 Accept 来进行内容协商响应。
  4. 使用 Vary 标头告诉客户端和中间体 Accept 标头对于缓存很重要。
  5. 重要的是要从历史中吸取教训,这样我们才不会重蹈覆辙。

笔记

参考