Skip to content

Nuxt 4.5 流式 SSR:哪些路由规则会悄悄禁用它 ​

译注:本文编译自 dev.to 文章《Nuxt 4.5 SSR Streaming: The Route Rules That Disable It》,作者 Parsa Jiravand,原文发布于 2026 年 10 月 4 日。原文链接见文末。

在 nuxt.config.ts 中打开 experimental.ssrStreaming: true,加载首页,首字节时间(TTFB)从 1.8 秒降到 40 毫秒——团队一片欢呼,随即把这个开关推给全站所有路由。两天后,有人发现 /pricing 页面(带 cache 路由规则,布局和组件都一样)一点没变快;再往后,生产环境开始报出团队从没见过的错误:ERR_HTTP_HEADERS_SENT。

这不是 Nuxt 的 bug。它是六条路由规则在悄悄把自己排除在流式渲染之外,再加上一个再普通不过的模式——在请求拦截器里设置 cookie——撞上了流式渲染唯一不允许你做的事:在响应开头已经发出之后,再改变响应的主意。

本文基于 Nuxt 4.5(对照 npm latest 标签上的 4.5.2 版本验证;experimental.ssrStreaming 随 4.5.0 于 2026 年 7 月 18 日发布)。Nuxt 3 已于 2026 年 7 月 31 日停止维护,如果你还在用 Nuxt 3,这个特性对你尚不存在——它仅限 4.5,且仍明确处于实验性阶段:需要主动开启,周边选项(比如后文会提到的 botRegex)还很年轻,Nuxt 团队仍在为清晰起见调整命名。

核心心智模型:先提交,还是先决定再提交 ​

SSR 流式渲染不改变 Nuxt 渲染什么,它改变的是响应何时被允许成为最终状态。

  • 缓冲式 SSR(默认行为,4.5 之前的每个 Nuxt 应用): Nuxt 先把整个页面渲染成内存中的字符串,完成之后才决定最终的 HTTP 状态码、响应头、cookie,然后一次性发出。在 Nuxt 完全决定完毕之前,浏览器什么都收不到。
  • 流式 SSR(experimental.ssrStreaming): Nuxt 先渲染外层壳——根布局、<head>、第一个异步边界之上的内容——一旦就绪,立刻确定状态码和响应头并刷入 socket,然后随着其余组件渲染完成,继续往这条已打开的连接里写入剩余内容。

这就是该特性的全部。速度收益是真实的:浏览器在 Nuxt 还在处理页面其余部分时就已经拿到字节,可以开始绘制和请求子资源。但提前提交响应头有一个单向门属性:一旦壳已刷出,Nuxt 就无法再改变响应的主意——不能改状态码、不能加响应头、不能设 cookie,也不能"其实应该重定向"。

用这个视角去看那份回退清单,每一条都能自我解释:这六条规则都需要对整个响应做出决定,而这些决定只能在任何内容发出之前完成。流式渲染取消了"之前"这个时机,所以 Nuxt 干脆不对这些路由做流式渲染。

六条会禁用流式渲染的路由规则 ​

根据 4.5 官方发布说明,带有以下任一 routeRules 的路由会自动回退到缓冲式渲染器,没有警告,也没有报错:

路由规则为什么无法流式渲染
redirect重定向是针对整个响应的不同状态码加 Location 头。壳一旦以 200 刷出,Nuxt 就无法再把它变成 30x。
cache缓存响应意味着缓存一份完整、最终的载荷。你没法合理地缓存"半个页面加一个稍后补完的承诺"。
isr增量静态再生成会把一份完成的 HTML 产物写入磁盘/CDN。和 cache 同样的问题,只是产物从内存缓存条目变成了构建产物。
swrstale-while-revalidate 仍需要一份完整响应作为"陈旧"版本对外提供,同时重新生成真正的版本——还是 cache 的问题,多了一个后台步骤。
noScripts这条规则的存在就是为了产出完全静态、没有 hydration JS 的输出。流式渲染的全部价值在于随应用 hydration 过程渐进渲染——这里根本没有可渐进 hydration 的应用。
ssr: false该路由被强制为 SPA 模式:服务端只发一个近乎空的壳,一切由客户端渲染。压根没有服务端渲染的正文可流式输出。

注意这个规律:清单上每一条规则都需要产出一件完成的、最终的东西——一份缓存载荷、一个静态文件、一次重定向、一个纯客户端壳——而流式渲染的整个机制就是故意发送一件未完成的东西。这两种思路在结构上不相容,不是"搭配起来有点别扭"而已。

保护爬虫,以及手动退出 ​

流式渲染还有一个不属于路由规则的内置例外:机器人和爬虫同样会拿到缓冲式响应,自动生效,这样搜索引擎收到的仍是一份完整的 HTML 文档,而不是它们可能处理不好的流:

typescript
export default defineNuxtConfig({
  experimental: {
    ssrStreaming: {
      // 调整哪些 user agent 算作爬虫
      botRegex: /googlebot|bingbot|my-internal-crawler/i,
    },
  },
})

如果你有一条路由按上述规则本可以流式渲染,但你还不信任它——比如它跑着你尚未审计过的业务逻辑——可以用它自己的路由规则强制保持缓冲:

typescript
export default defineNuxtConfig({
  routeRules: {
    '/checkout/**': { streaming: false },
  },
})

关键概念: 六条规则的回退清单是 Nuxt 在自动保护你;streaming: false 则是你手动保护自己,用于那些自动清单不知道有风险的路由。

响应被过晚修改时会发生什么 ​

这是发布说明里作为注意事项描述、而真实应用以线上故障形式发现的部分。2026 年,社区认证模块 nuxt-auth-sanctum 发布了一个响应拦截器,它——和很多认证、会话代码一样——试图在 SSR 期间往即将发出的响应上设置 cookie。在流式渲染下,这个拦截器在壳已经刷出之后才运行。结果(记录在 issue #669 中):使用该模块的每个页面的每次 SSR 请求都抛出 ERR_HTTP_HEADERS_SENT,渲染在流中途中断,访客拿到的是一个 HTTP 200 加一个死掉的、只写了一半的页面——甚至不是一次干净的错误,因为在出错之前状态码就已经作为成功提交了。维护者在 PR #702 中发布了修复,让该模块感知流式渲染。

这就是这类故障的通用形态,而且不限于那一个模块:任何试图在渲染过程中设置 cookie、响应头或状态码的代码——认证拦截器、A/B 测试插件、地域重定向检查——在你为它所在的路由打开流式渲染的那一刻,都是这种崩溃的候选者。

边界情况与坑 ​

  • 嵌套路由规则会叠加。 如果父路径带 cache 而子路径没有覆盖它,子路径也会继承回退行为。在假设某个页面会流式渲染之前,先审计路由规则的继承关系——检查匹配其完整路径的所有规则,而不只是你为它写的那一条。
  • 这仍是 experimental。 Nuxt 团队在发布 4.5.0 之后仍在打磨这个特性——issue #36250 指出 botRegex 这个名字没有说清匹配方向(结果是"把爬虫排除在流式渲染之外")。该问题最终通过澄清文档而非重命名选项解决,但讨论本身说明,发布后不久其形态和命名仍在被质疑。预计措辞和默认值会在 4.x 系列中继续变动;在把某个选项名记进肌肉记忆之前,先重新查文档。
  • 缓存与路由规则还拿到过一个真实的安全补丁。 Nuxt 4.5.1 修复了一个影响 cache、swr、isr 路由规则的跨用户载荷泄露问题,同时修复了一个路由规则授权绕过和一个独立的 server island RCE。如果你用了这三条规则中的任何一条,请确认版本已超过 4.5.1;如果你在升级前于 CDN 后面做了缓存,请清空它,因为泄露的 _payload.json 可能已经躺在缓存里了。这和跨请求状态泄露属于同一类 bug:一个用户的服务端渲染数据出现在另一个用户的响应里。
  • 流式渲染不会让已缓存的路由更快。 如果一条路由本来就从 cache/isr/swr 提供响应,它很可能已经很快了——流式渲染在这里既加不了什么,也减不了什么,它只是不适用。
  • 一条路由可能在运行时进出回退清单。 如果路由规则是条件应用的(某些 Nuxt 配置会按环境或按部署计算 routeRules),一条在预发环境流式渲染的路由,可能在生产环境悄悄停止流式渲染。如果各环境 TTFB 数字对不上,请对比解析后的路由规则,而不只是源文件。

最佳实践 ​

  • 先对一个路由组开启,观察一天 TTFB 和错误率,再逐步扩大。不要基于一个基准测试页面就全站打开。
  • 审计渲染期间触碰响应的一切代码——认证模块、A/B 测试、地域重定向、设置 cookie 的自定义服务端中间件——再对它们所在的路由启用流式渲染。如果来不及快速审计,就在该路径上先用 routeRules: { streaming: false },之后再回头看。
  • 不要指望带 cache、isr、swr、redirect、noScripts 或 ssr: false 的路由因为这个开关而变快。 结构上不可能;去别处测量。
  • 特别关注 4.5.x 补丁线,而不只是"在用 Nuxt 4"——路由规则缓存的安全修复是在补丁版本而非次版本中落地的。
  • 在该特性保持实验性期间,每次升级前重读实验性特性文档;选项形态仍在调整。

常见问题 ​

开启 experimental.ssrStreaming 会改变我的 routeRules 吗? 不会。它改变的是 Nuxt 投递路由响应的方式。你的 routeRules 不变;Nuxt 只是读取它们,按路由决定是允许流式渲染还是必须回退到缓冲。

为什么我打开流式渲染后,缓存页面没有变快? 因为 cache 正是强制缓冲的六条路由规则之一。流式渲染对该路由根本不适用——请改去没有 cache/isr/swr/redirect/noScripts/ssr: false 规则的路由上寻找 TTFB 收益。

我能强制一条带 cache 规则的路由流式渲染吗? 不能——这不是 Nuxt 暴露的设置,理由也很充分:缓存响应需要一份完成的响应可缓存,而流式渲染产不出这个。但反方向可以:用 routeRules: { '/path': { streaming: false } } 强制一条本可流式渲染的路由保持缓冲。

Nuxt 4.5 中的 SSR 流式渲染稳定吗? 不稳定。它位于 nuxt.config.ts 的 experimental 下,需要主动开启,且相关选项名(如 botRegex)在 4.5.0 首发后仍在打磨。把它当作可以试点、但不能假定已定稿的东西。

这会影响 Google 抓取我网站时看到的内容吗? 不会,这是设计使然。Nuxt 通过 botRegex 识别机器人和爬虫,并向它们提供旧的缓冲式、完整渲染的 HTML——流式渲染专门面向能渐进渲染的真实浏览器。

如果我的代码设置 cookie 太晚,实际报什么错?ERR_HTTP_HEADERS_SENT——Node 在响应头已发出后再次写入时的标准错误。在流式渲染下,这个时刻在壳刷出时就到来了,比大多数修改响应的代码所预期的要早得多。

速查表 ​

typescript
// nuxt.config.ts — SSR 流式渲染、路由规则与逃生舱

export default defineNuxtConfig({
  experimental: {
    ssrStreaming: {
      // 无论此正则如何,机器人/爬虫始终拿到缓冲式响应
      botRegex: /googlebot|bingbot/i,
    },
  },

  routeRules: {
    // 以下六条规则始终自动回退到缓冲式渲染:
    //   redirect | cache | isr | swr | noScripts | ssr: false
    '/pricing': { cache: { maxAge: 60 } },      // 缓冲——缓存需要完成的响应
    '/old-docs': { redirect: '/docs' },          // 缓冲——状态/响应头在渲染前决定
    '/landing': { isr: 3600 },                   // 缓冲——写入完成的静态产物
    '/catalog/**': { swr: 300 },                 // 缓冲——同 cache,外加再验证
    '/print/**': { noScripts: true },            // 缓冲——没有可流式服务的 hydration
    '/legacy-app/**': { ssr: false },            // 缓冲——SPA 壳,没有服务端正文可流

    // 对一条本会流式渲染、但你还不信任的路由手动退出:
    '/checkout/**': { streaming: false },
  },
})
存在的路由规则是否流式渲染原因
无✅ 是没有任何东西强制预渲染决定
redirect❌ 否状态/响应头在正文存在之前就已决定
cache❌ 否需要一份完成的响应来缓存
isr❌ 否写入一份完成的静态产物
swr❌ 否同 cache,外加再验证步骤
noScripts❌ 否没有 hydration 可供流式服务
ssr: false❌ 否纯客户端壳,没有服务端正文

原文链接 ​