记一次 Next.js + K8s + CDN 缓存导致 RSC 泄漏的排查与修复

本文记录了我在一个 Next.js(App Router)项目部署到 K8s,并通过云厂商 CDN 加速时,遇到的一个生产问题:页面偶发性"变成一堆奇怪的代码",比如 $Sreact.fragmentOutletBoundary 等文本直接渲染到页面上。

最终确认为 CDN 的缓存策略与 RSC 响应变体不兼容所致。下面是完整的排查路径与修复方案,期望对类似场景有所参考。


背景与架构

  • 前端框架:Next.js(App Router)
  • 部署环境:Kubernetes(K8s)
  • 加速:云厂商 CDN(仅支持"目录或后缀"规则配置,不支持按请求头维度的缓存键自定义)

项目包含:

  • App Router 页面与静态资源;

现象与线索

页面"变成代码"

  • 部分页面在刷新预热后短时间内正常,但过一段时间就会出现页面返回 text/x-component,正文类似:
bash 复制代码
$Sreact.fragment
OutletBoundary
ClientPageRoot
...
  • 响应头包含:
less 复制代码
Content-Type: text/x-component
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-nextjs-stale-time: 300

这其实是 Next.js 的 RSC(React Server Components)流式响应,用于客户端路由和预取;不应该被 CDN 给普通页面请求复用。

快速自检(可复制粘贴):

bash 复制代码
# 1) 看响应是否为 text/x-component(应为 text/html)
curl -s -I https://your-domain/zh-cn/company | grep -i content-type
bash 复制代码
# 2) 看 CDN 是否命中(不同厂商字段略有差异)
curl -s -I https://your-domain/zh-cn/company | grep -iE 'age|x-cache|cdn-cache'
bash 复制代码
# 3) 模拟 RSC 请求(Next.js 13+ 通用)
curl -s \
  -H "rsc: 1" \
  -H "accept: text/x-component" \
  https://your-domain/zh-cn/company | head -c 200

根因分析

CDN 错误缓存了 RSC 响应变体

  • App Router 下,Next.js 对页面和 RSC 流采用不同的响应形态;RSC 响应以 text/x-component 返回,并通过 Vary 控制不同的请求头变体(如 rscnext-router-prefetch 等)。
  • 当 CDN 仅支持"目录或后缀"策略、无法将这些请求头并入缓存键时,就会发生"带预取/路由头的 RSC 响应被缓存",随后被误用于普通页面请求,导致页面显示 RSC 流文本。

修复方案

1. CDN 只缓存"静态白名单",页面路径不缓存

由于 CDN 不支持按请求头区分缓存键,采用"白名单缓存静态、黑名单不缓存页面"的策略最稳妥:

  • 缓存后缀白名单(TTL 建议 7--30 天):
    • *.js, *.css, *.png, *.jpg, *.svg, *.webp, *.ico, *.woff, *.woff2
  • 缓存目录白名单(TTL 建议 30 天):
    • /_next/static/*(Next 构建产物路径,带构建指纹,长 TTL 安全)
  • 页面目录不缓存(或 TTL=0):
    • /zh-cn/*, /zh-hk/*, /en-us/*(项目页面均位于语言前缀路径下)
  • 默认规则:不缓存(或遵循源站)。

同时,预热(prewarm)只预热静态资源;不要预热页面路径,避免将 RSC 变体推入 CDN 缓存。

2. 源站中间件为 RSC/预取请求设置 no-store

src/middleware.ts 中对带有 RSC/预取特征的请求头添加 Cache-Control: private, no-store,减少被 CDN 误缓存的几率。核心逻辑类似:

ts 复制代码
import { NextRequest, NextResponse } from 'next/server'

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)'],
}

export function middleware(req: NextRequest) {
  const isRSCRequest =
    req.headers.get('rsc') === '1' ||
    req.headers.get('next-router-prefetch') === '1' ||
    req.headers.get('next-router-segment-prefetch') === '1' ||
    (req.headers.get('accept') || '').includes('text/x-component') ||
    (req.headers.get('accept') || '').includes('application/vnd.nextjs.router+json')

  const res = NextResponse.next()
  if (isRSCRequest) res.headers.set('Cache-Control', 'private, no-store')
  return res
}

注意:CDN 必须"遵循源站缓存指令",该策略才会生效;否则还是要靠规则层面的"页面不缓存"。

提醒:务必确认 CDN 控制台"缓存规则"→"遵循源站 Cache-Control"处于开启状态,否则中间件下发的 no-store 会被 CDN 直接忽略。


发布与验证

  1. 应用源站中间件变更
  • 部署更新并滚动重启应用,使中间件的 Cache-Control: private, no-store 生效。
  • 确认源站响应已包含预期的 Cache-ControlContent-Type(页面为 text/html)。
  1. 清理并预热 CDN
  • 清理缓存后,只预热静态资源目录(如 /_next/static/*)。
  • 不预热页面目录(如 /zh-cn/*),避免误缓存 RSC。
  1. 验证页面与静态资源
  • 页面 URL(例如 /zh-cn/company):
    • 期望 Content-Typetext/html(而非 text/x-component)。
    • 观察 Age 与缓存命中是否符合"不缓存页面"的预期。
  • 静态资源(如 /_next/static/...):
    • 期望大 TTL 且经常命中 CDN。

兼容性与权衡

  • ISR(增量静态再生成):

    • 响应头可能含 x-nextjs-prerenderx-nextjs-stale-time 等提示;若 CDN 不遵循源站策略且易误缓存,建议对容易出问题的页面关闭 ISR(在页面导出 revalidate = 0),仅让源站享受缓存;将 CDN 缓存收益保留给静态资源。
  • 图片优化:

    • 如果启用 Next 图片优化(/_next/image/*),可以为该目录设置合适的 TTL;若项目配置了 images.unoptimized: true,则直接走静态白名单即可。
  • 中间件与国际化重定向:

    • 中间件 matcher 排除了 api_next/static 等路径,不会影响内部 API 与静态资源;
    • 国际化按语言前缀(/zh-cn, /zh-hk, /en-us)重定向的场景,仍应遵循"页面不缓存"的 CDN 规则。

最终清单(Checklist)

  • CDN:只缓存静态后缀与 /_next/static/*,页面目录不缓存,默认不缓存。
  • 源站:中间件为 RSC/预取请求设置 Cache-Control: private, no-store
  • 部署:更新配置并重启,清理 CDN 缓存并仅预热静态资源。
  • 验证:确认页面返回 HTML、静态资源缓存命中,且不再出现 RSC 文本。

结语与提问

本文的解决方案基于"CDN 仅支持目录/后缀规则"的现实约束,通过白名单缓存静态与禁止页面缓存,辅以中间件 no-store 将问题最小成本化解决。如果你有更好的思路(比如在不支持请求头缓存键的 CDN 上,仍能优雅地区分 RSC 变体),或者遇到过类似但更复杂的场景,欢迎交流分享:

  • 你是否也遇到过 RSC 响应被 CDN 误缓存导致页面渲染异常?
  • 在仅支持目录/后缀规则的 CDN 上,你还有哪些更稳妥或更高效的做法?

(本文未包含任何真实域名或链接,示例路径仅用于说明)

相关推荐
却尘2 小时前
一个"New Chat"按钮,为什么要重构整个架构?
前端·javascript·next.js
168清纯女高2 小时前
路由动态Title实现说明(工作问题处理总结)
前端
二川bro3 小时前
第30节:大规模地形渲染与LOD技术
前端·threejs
景早3 小时前
商品案例-组件封装(vue)
前端·javascript·vue.js
AI大模型3 小时前
小白也能训大模型!Hugging Face用「200页手册」亲自教学,连踩的坑都告诉你了...
程序员·llm·agent
不说别的就是很菜3 小时前
【前端面试】Vue篇
前端·vue.js·面试
IT_陈寒3 小时前
Java 17实战:我从老旧Spring项目迁移中总结的7个关键避坑点
前端·人工智能·后端
倚肆3 小时前
CSS 动画与变换属性详解
前端·css