
上周复查一个 App Router 项目的首屏瀑布图,发现一个尴尬事实:接口其实不算慢,慢的是页面“迟迟不露面”。Chrome 里空白好久,然后内容一口气砸下来。团队都以为是 RSC 没开好。其实 RSC 开了,只是把流式渲染的水龙头拧死了。
React Server Components 在 2026 年已经不是实验品。Next.js、生产环境都在用。但很多团队把它当成“默认更快的开关”。它不是开关,是边界:数据在哪边取、JS 往哪边送、哪些 HTML 可以先流出去。边界画错了,性能账会从浏览器挪到服务器,数字更好看,用户照样骂。
顶层 await 会堵死外壳
最常见的坑几乎一眼能认出来:
// app/dashboard/page.tsx
export default async function Page() {
const user = await getUser() // 慢
const metrics = await getMetrics() // 更慢
return (
<main>
<Header user={user} />
<Metrics data={metrics} />
</main>
)
}
在 page 顶部串行 await,服务器要等全部数据齐了才开始吐第一段 HTML。流式渲染在协议层还在,但你没有可先发送的壳。用户看到的是白屏,不是“骨架先到、数字后到”。
LogRocket 等工程文把这类问题归纳得很直:壳被慢请求堵住时,Suspense 再漂亮也轮不到上场。你需要的是“先返回能画的东西”。
改法不复杂:先返回静态壳,把慢数据丢进带 Suspense 的子树。
import { Suspense } from 'react'
export default function Page() {
return (
<main>
<HeaderShell />
<Suspense fallback={<MetricsSkeleton />}>
<MetricsPanel />
</Suspense>
</main>
)
}
async function MetricsPanel() {
const metrics = await getMetrics()
return <Metrics data={metrics} />
}
HeaderShell 不依赖慢接口时,首字节可以立刻走。指标区自己转圈。体感差通常就出在“整页等齐”上。
再补一条:layout 里也不要随便顶层 await 用户会话或远程配置。layout 卡住,下面所有子路由一起陪葬。能下沉到具体 segment 的数据,就别抬到根布局。
服务端瀑布流还是瀑布流
把请求挪到服务端,不等于并行。父组件 await 完再渲染子组件,子组件再 await,链路照样串行。只是瀑布从图里的浏览器 Network 挪到了 Node 进程。监控面板上“客户端请求变少了”,你可能只是把瀑布藏起来了。
更稳的写法是:同层互不依赖的数据,在同一层一起发起;真有依赖再串。别为了“组件干净”把请求拆成三层嵌套 await。
async function DashboardBody() {
const [orders, inventory] = await Promise.all([
getOrders(),
getInventory(),
])
return (
<>
<Orders data={orders} />
<Inventory data={inventory} />
</>
)
}
有依赖时再显式串,并在 UI 上分开边界:先出订单列表,库存卡片单独 Suspense。不要为了一次 Promise.all 把无关模块绑死在同一个最慢源上。
图:流式外壳与慢数据分区

"use client" 往上爬
另一个慢性病:为了给一个按钮加 onClick,把半棵树标成 Client Component。"use client" 是入口,不是装饰。入口一抬高,下面整棵子树的服务器优势就没了,包体积和 hydration 成本一起回来。
习惯是:Client 尽量贴叶子。列表、布局、数据拼接留在 Server;交互控件单独拆成小组件。需要共享状态时,用细的 Client 边界包住状态,而不是把整页变成客户端岛。
我见过最夸张的一次,是把整个 app/(dashboard)/layout.tsx 标成 client,只因为侧边栏要折叠。折叠状态用一个 30 行的 Client 组件就够了,犯不着把数据层拽回浏览器。
不是所有数据都该阻塞首屏
RSC + Streaming 的收益,一半来自“分清关键路径”。用户身份条、首屏列表可以优先;次要统计、推荐位、审计旁路可以后到。把所有 fetch 都当成 LCP 必需品,等于自己关掉 partial streaming。
Next.js 里还可以配合 Partial Prerendering:静态壳预渲染,动态洞按请求流。前提仍是:你得先在组件树里画出哪些是壳、哪些是洞。洞太多且彼此耦合,PPR 也救不了设计问题。
缓存策略也要跟着边界走。能静态的壳别因读一次 cookie 整页动态;能按 tag 失效的数据,别动不动 revalidatePath 扫荡整站。RSC 让数据获取更分散,缓存键若仍按“整页”思考,你会得到一堆难以解释的新鲜度问题。
我怎么排查
- 看 TTFB 和 First Contentful Paint 的间距。TTFB 还行但 FCP 很晚,多半是服务器在等数据才吐壳。
- 搜
page.tsx/layout.tsx里的顶层await,问一句:这段不返回,用户连导航栏都看不到吗? - 数
"use client"的位置。出现在 layout 或大段业务容器上,就要警惕。 - 用 React / Next 的调试信息看 Suspense 边界是否真的拆开了慢源。
- 在预发制造一个 2 秒的假延迟接口,只挂在次要模块上——若首屏跟着慢 2 秒,边界就画错了。
小结
RSC 不会自动让你的站变快。它把性能问题从“客户端会不会 hydrate 爆炸”换成了“服务端会不会把壳堵死、会不会在 Node 里重演瀑布流”。
先给用户一个能看的外壳,再让慢数据自己入座。这个顺序对了,流式才有意义。其余的优化——缓存、并行请求、叶子级 Client——都排在这之后。
2026 年还在纠结要不要上 RSC 的团队,不如先回答更土的问题:你的首屏,到底在等谁。


