服务器端渲染实时 React,无需熔断服务器

2025-06-09

服务器端渲染实时 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>,
}
Enter fullscreen mode Exit fullscreen mode

每次作者变更,您都需要使归属于该作者的每一篇博文的缓存失效。这只是一个简单的一对多关系,一个内容足够丰富的网站,其内容引用会深入多层。即使您付出了所有努力来维护内容关系树,下次重建网站时,仍然需要重新获取所有内容,从而带来显著的延迟。

但从宏观角度来看,所有这些都是完全没必要的优化对话。对于大多数团队来说,只要能够快速恢复错误的部署,网站渲染时间在一分钟和五分钟之间其实并无太大区别。但在 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 策略对于我们尽快更新内容的要求来说仍然是成功的,并且最终继续成为静态网站渲染方式的支柱。

前提是,我们不应该尝试一次性重新渲染整个网站,而是利用网站流量来触发增量式重新渲染(如果缓存内容过时)。从高层次来看,它看起来如下所示:

  1. 为“构建版本”和“内容版本”保留一组键值对。
  2. 如果 CMS 中发布了任何内容,则会触发 webhook 并且“内容版本”会增加。
  3. 如果网站已部署,则增加构建版本。
  4. 如果上次呈现的页面是较旧的版本或内容版本,请重新呈现该页面并清除缓存。

“内容版本”有点幼稚,因为它会导致大量不必要的重新渲染,但它比尝试使用 Contentful webhook 来维护 CMS 内容引用的一致图形数据库要简单 10 倍,这将需要进行更有选择性的重新渲染(正如我之前在“作者”引用问题中所解释的那样)。

2019 年冬天,主要是为了迎接爱荷华州和其他初选的开始,我们开始了一系列建筑改进。

首先,我们将所有前端资源都迁移到了 CDN 子域名。这在高流量网站中已经很常见了,也是我一直以来的待办事项清单之一,但一直没能加入到冲刺阶段。

不过,我们做了一些有趣的事情。每次部署都会在 CDN 中创建一个新的、唯一命名且不可变的文件夹,所有资源都会放入其中。例如,

https://cdn.elizabethwarren.com/deploy/1cc2e8207789dc8c0a3f83486cae16a3cd3effa8b970f6306c1435c31014a560890f5236722af8d7ed3cfec76107508ffd82b2eb872b00e3ddf3f88012ead904/build/6.5d30e50ab08bb11f9cf8.js
Enter fullscreen mode Exit fullscreen mode

这确保了无论您从浏览器缓存中看到的是旧版网站,还是我们端提供的旧版网站,资产始终存在,就像最初部署时一样。随着我们深入研究所使用的服务器端渲染策略,这一点将变得越来越重要。

这个唯一文件夹名称的另一个好处是,它允许我们安全地将高max-age值应用于cache-control标头,确保您的浏览器将文件保留相当长一段时间,而不是在您下次访问时重新请求它。对在部署之间更改内容但不一定更改文件名的文件使用 max-age 是一种让您的用户陷入非常严重的缓存问题的快速方法。我们的 webpack 配置对 Javascript 块文件的名称进行了散列,但某些文件没有唯一的散列文件名(特别是 webpack清单文件)。(*我还应该注意,某些在部署之间没有发生变化的文件(例如字体)被保存在一致的位置,并且没有在唯一的构建文件夹下重新部署)。

一旦我们获得了 CDN 提供的所有字体、图片、CSS 和 JavaScript,下一步就是在后台工作程序上执行服务器端渲染,并将 HTML 存储在 Redis 中,这样 Web 服务器就只负责提供已放入 Redis 的 HTML。新的 SSR 策略如下所示:

  1. 为“构建版本”和“内容版本”保留一组键值对。
  2. 如果 CMS 中发布了任何内容,则会触发 webhook 并且“内容版本”会增加。
  3. 如果网站已部署,则增加构建版本并将构建文件推送到 CDN。
  4. 当请求到达时,Web 服务器会立即提供 Redis 缓存中的任何页面。**
  5. 如果我们提供的页面已经过时,则向 Redis 队列添加一个项目以通知后台工作者该页面需要重新呈现。
  6. 后台工作程序最终会重新渲染页面,将 HTML 推送到 Redis 缓存并清除该页面的 Cloudflare 缓存。

** 这些过时的页面很可能来自网站之前的版本,所以之前提到的那个独特的构建文件夹非常重要!它就像一台迷你的回溯机器。

这两项架构变更带来了立竿见影的显著改进,提升了我们堆栈的稳定性。由于这些变更非常成功,这本应是我们在二月初初选开始前进行的最后一次架构变更。不幸的是,Heroku 在一月份经历了几次严重的服务中断。其中包括日志记录等重要功能的服务中断,持续了超过 24 小时,以及整个平台的瘫痪。因此,就在爱荷华州党团会议召开前一周多,我担心 Heroku 会出问题,于是召集了一些团队成员讨论是否应该迁移到静态网站,最终我们决定这么做。

做出这一决定的部分安全保障在于,切换过程中涉及的大部分工作是在 Cloudflare Workers 上创建边缘路由,因为我们的后端 Web 服务器只需将它们正在生成的数据指向 S3 而不是 Redis。以下是新的 SSR 策略,以及我们交付到生产环境的最后一个架构变更。

  1. 为“构建版本”和“内容版本”保留一组键值对。
  2. 如果 CMS 中发布了任何内容,则会触发 webhook 并且“内容版本”会增加。
  3. 如果网站已部署,则增加构建版本并将构建文件推送到 CDN。
  4. 当请求到达时,Cloudflare Worker 会从 CDN 中提取 HTML(* 也有简单的重试逻辑,因为 S3 请求很少但偶尔会失败)。
  5. 处理请求后,Cloudflare Worker 将请求转发到 Web 服务器。
  6. Web 服务器接收请求,如果页面被标记为陈旧,则 Web 服务器会将一个项目添加到 Redis 队列中,以通知后台工作者该页面需要重新渲染。
  7. 后台工作程序最终会重新渲染页面,将 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
PREV
我的最小 Web 开发设置
NEXT
在 VS Code 中管理多个 GitHub 帐户:综合指南