服务器端渲染实时 React,无需熔断服务器
这篇文章将深入探讨 ElizabethWarren.com 如何最终实现从无头 CMS 动态重新渲染内容更新、缓存所有内容,并通过 S3 存储桶为整个网站提供服务。阅读本文需要读者对 React 等 Web 技术有基本的了解。
我想分享这篇文章,主要是因为在整个活动期间,我对网上关于在生产中扩展 React 服务器端渲染的内容的缺乏感到非常沮丧,我希望这篇文章中的一些想法有一天能够对某人有所帮助。
相关说明:我认为这篇文章会是一次有趣的演讲,如果你知道一个正在寻求提案的很棒的会议,请与我们联系!
网络从仅仅是位于网络服务器上的静态文件,发展到单片网络服务器,再到微服务,而现在又回到了部署静态文件的趋势,这真是令人着迷。
从运营角度来看,静态网站非常出色,与 Web 服务器相比,它们几乎没有成本,并且拥有您选择的对象存储提供商(最常见的是 AWS S3)的高正常运行时间保证。如今,静态网站只需要一个构建系统,该系统可以根据命令构建网站并将其推送到对象存储。如果您想要更高级的功能,甚至可以跨区域或云提供商设置多个存储桶,以增强冗余。如果您需要与静态网站一起维护一些轻量级的自定义路由,可以使用 Cloudflare Workers 或 Lambda@Edge 等服务在边缘运行代码。
通常,将 React 应用程序部署为静态网站的第一个障碍是服务器端渲染所有页面。
你可能会问,什么是服务器端渲染?在 NodeJS 进程中渲染 React 应用程序被称为服务器端渲染 (SSR),它只是一种更贴切的说法,指的是你想在浏览器上下文之外生成页面上的所有 HTML。虽然并非每个 React 项目都要求服务器端渲染(例如,内部仪表板只需客户端渲染就足够了),但如果你想要网站访问者在加载时立即看到页面内容(例如:文章或落地页),或者你希望 Google 抓取你的网页,那么服务器端渲染是必不可少的。
不过,React 本质上是一个 UI 库,所以你需要额外连接一些资源才能将 React 组件在服务器端渲染成静态文件。市面上有很多优秀的框架可供选择,比如 NextJs 和 GatsbyJs,它们可以让你轻松实现这一点。
但是,如果您的业务需求包括能够尽可能快地推送内容更新,那么您将遇到问题。服务器端渲染整个网站并非瞬时完成。而且,如果您的网站不仅仅是一个内容网站(例如:您有一百万个个人资料页面),那么使用 Next 或 Gatsby 将所有这些用户个人资料导出为静态页面并非易事。因此,就本文而言,我们仅讨论内容网站。
服务器端渲染所需的时间实际上并没有一个平均时间,因为它完全取决于渲染的组件。但一个复杂的页面渲染时间可能超过 100 毫秒。在静态站点中,你可以通过将构建系统的工作分配到多个核心来优化这一点(例如:看看 Gatsby 是如何做到的),但最终你可能会遇到另一个问题,即获取内容的延迟。
每个 Web 技术栈都各不相同,但现代 Web 开发中一个常见的模式是使用无头 CMS。无头 CMS 简单来说,存储所有内容的内容管理系统与支持界面的应用程序分离,内容通过 API 从 CMS 获取。
如果您使用的是无头CMS(例如Contentful),您的静态构建系统渲染页面的速度将取决于它从CMS在线获取内容的速度。实际上,这会在您开始渲染页面之前增加几百毫秒的时间。
一个简单的加速方法是使用分页并一次请求多个页面。但是,如果您的网站包含数千个单词的页面(或计划),那么分页就会开始成为问题,因为网络负载大小和 NodeJS 进程的内存耗尽都会造成问题。
减少获取内容时间的另一种方法是将这些 CMS 响应缓存在构建系统可以访问的数据库中,但现在您刚刚创建了一个非常“有趣”的缓存失效问题来解决。
例如,假设您有一篇博客文章的内容模型,如下所示,
{
"title": String,
"publishedAt": Date,
"content": String,
"author": <Reference:Author>,
}
每次作者变更,您都需要使归属于该作者的每一篇博文的缓存失效。这只是一个简单的一对多关系,一个内容足够丰富的网站,其内容引用会深入多层。即使您付出了所有努力来维护内容关系树,下次重建网站时,仍然需要重新获取所有内容,从而带来显著的延迟。
但从宏观角度来看,所有这些都是完全没必要的优化对话。对于大多数团队来说,只要能够快速恢复错误的部署,网站渲染时间在一分钟和五分钟之间其实并无太大区别。但在 elizebthwarren.com 上,我们必须小心翼翼地协调网站更新,使其与活动的其他部分保持同步(也就是说,所有更新都必须尽快完成,而且通常没有任何提前通知)。
这意味着,在大部分活动期间,网站架构本质上就是一个缓存层,位于 Web 服务器前端,始终会输出网站的最新版本。清除缓存,一切就绪!
在活动期间,随着流量、技术要求和网页内容的不断增长,我们的架构也经历了多次演变。以下是简要概述:
发布(2019 年 2 月):Wordpress 后端、React 客户端渲染
2019 年春季:开始将 Contentful 与客户端 React 组件集成
2019 年 6 月:Heroku 上的 NodeJS 后端,将 Wordpress 模板转换为 Mustache 模板,继续在客户端渲染现有的 React 组件
2019 年仲夏:Redis 缓存 Contentful 数据
2019 年夏末:服务器端渲染 React 组件
2019 年秋季:在 Redis 中存储服务器端渲染的页面
2019 年 12 月:后台工作人员进行 SSR,将资产转移到 cdn。
2020 年 2 月:移至完全静态网站。
自从我们放弃 Wordpress 之后,我们经常会把更多东西放到 Redis 里,并在流量高峰期(比如辩论或其他热门话题)添加更多服务器。虽然这在大多数情况下都“有效”,但我不喜欢总是担心系统会在最糟糕的时刻崩溃。
尽管如此,我们实施的整体 SSR 策略对于我们尽快更新内容的要求来说仍然是成功的,并且最终继续成为静态网站渲染方式的支柱。
前提是,我们不应该尝试一次性重新渲染整个网站,而是利用网站流量来触发增量式重新渲染(如果缓存内容过时)。从高层次来看,它看起来如下所示:
- 为“构建版本”和“内容版本”保留一组键值对。
- 如果 CMS 中发布了任何内容,则会触发 webhook 并且“内容版本”会增加。
- 如果网站已部署,则增加构建版本。
- 如果上次呈现的页面是较旧的版本或内容版本,请重新呈现该页面并清除缓存。
“内容版本”有点幼稚,因为它会导致大量不必要的重新渲染,但它比尝试使用 Contentful webhook 来维护 CMS 内容引用的一致图形数据库要简单 10 倍,这将需要进行更有选择性的重新渲染(正如我之前在“作者”引用问题中所解释的那样)。
2019 年冬天,主要是为了迎接爱荷华州和其他初选的开始,我们开始了一系列建筑改进。
首先,我们将所有前端资源都迁移到了 CDN 子域名。这在高流量网站中已经很常见了,也是我一直以来的待办事项清单之一,但一直没能加入到冲刺阶段。
不过,我们做了一些有趣的事情。每次部署都会在 CDN 中创建一个新的、唯一命名且不可变的文件夹,所有资源都会放入其中。例如,
https://cdn.elizabethwarren.com/deploy/1cc2e8207789dc8c0a3f83486cae16a3cd3effa8b970f6306c1435c31014a560890f5236722af8d7ed3cfec76107508ffd82b2eb872b00e3ddf3f88012ead904/build/6.5d30e50ab08bb11f9cf8.js
这确保了无论您从浏览器缓存中看到的是旧版网站,还是我们端提供的旧版网站,资产始终存在,就像最初部署时一样。随着我们深入研究所使用的服务器端渲染策略,这一点将变得越来越重要。
这个唯一文件夹名称的另一个好处是,它允许我们安全地将高max-age
值应用于cache-control
标头,确保您的浏览器将文件保留相当长一段时间,而不是在您下次访问时重新请求它。对在部署之间更改内容但不一定更改文件名的文件使用 max-age 是一种让您的用户陷入非常严重的缓存问题的快速方法。我们的 webpack 配置对 Javascript 块文件的名称进行了散列,但某些文件没有唯一的散列文件名(特别是 webpack清单文件)。(*我还应该注意,某些在部署之间没有发生变化的文件(例如字体)被保存在一致的位置,并且没有在唯一的构建文件夹下重新部署)。
一旦我们获得了 CDN 提供的所有字体、图片、CSS 和 JavaScript,下一步就是在后台工作程序上执行服务器端渲染,并将 HTML 存储在 Redis 中,这样 Web 服务器就只负责提供已放入 Redis 的 HTML。新的 SSR 策略如下所示:
- 为“构建版本”和“内容版本”保留一组键值对。
- 如果 CMS 中发布了任何内容,则会触发 webhook 并且“内容版本”会增加。
- 如果网站已部署,则增加构建版本并将构建文件推送到 CDN。
- 当请求到达时,Web 服务器会立即提供 Redis 缓存中的任何页面。**
- 如果我们提供的页面已经过时,则向 Redis 队列添加一个项目以通知后台工作者该页面需要重新呈现。
- 后台工作程序最终会重新渲染页面,将 HTML 推送到 Redis 缓存并清除该页面的 Cloudflare 缓存。
** 这些过时的页面很可能来自网站之前的版本,所以之前提到的那个独特的构建文件夹非常重要!它就像一台迷你的回溯机器。
这两项架构变更带来了立竿见影的显著改进,提升了我们堆栈的稳定性。由于这些变更非常成功,这本应是我们在二月初初选开始前进行的最后一次架构变更。不幸的是,Heroku 在一月份经历了几次严重的服务中断。其中包括日志记录等重要功能的服务中断,持续了超过 24 小时,以及整个平台的瘫痪。因此,就在爱荷华州党团会议召开前一周多,我担心 Heroku 会出问题,于是召集了一些团队成员讨论是否应该迁移到静态网站,最终我们决定这么做。
做出这一决定的部分安全保障在于,切换过程中涉及的大部分工作是在 Cloudflare Workers 上创建边缘路由,因为我们的后端 Web 服务器只需将它们正在生成的数据指向 S3 而不是 Redis。以下是新的 SSR 策略,以及我们交付到生产环境的最后一个架构变更。
- 为“构建版本”和“内容版本”保留一组键值对。
- 如果 CMS 中发布了任何内容,则会触发 webhook 并且“内容版本”会增加。
- 如果网站已部署,则增加构建版本并将构建文件推送到 CDN。
- 当请求到达时,Cloudflare Worker 会从 CDN 中提取 HTML(* 也有简单的重试逻辑,因为 S3 请求很少但偶尔会失败)。
- 处理请求后,Cloudflare Worker 将请求转发到 Web 服务器。
- Web 服务器接收请求,如果页面被标记为陈旧,则 Web 服务器会将一个项目添加到 Redis 队列中,以通知后台工作者该页面需要重新渲染。
- 后台工作程序最终会重新渲染页面,将 HTML 推送到 CDN 并清除该页面的 Cloudflare 缓存。
这一策略巩固了网站的各个方面,首先由 CDN 提供服务,并将服务器端渲染的所有计算和网络负载卸载到后台进程。同样重要的是,它继续实现了我们的目标,即允许 CMS 编辑者发布更改并在几秒钟内看到生产环境中的更新。
然后,爱荷华州党团会议之夜就来了。正值黄金时段,确切地说是东部时间晚上 9 点之前,Heroku 又一次遭遇了重大平台故障……不过,这可是个笑话,因为 ElizabethWarren.com 完全瘫痪了!😎
如果您想了解更多有关我们在 ElizabethWarren.com 上所做的工作,请查看此 Twitter 帖子!
链接已锁定:https://dev.to/itsjoekent/server-side-rendering-react-in-realtime-without-melting-your-servers-21ej