Next.js Loading 与 Streaming
Next.js App Router 的 loading.tsx 以 Suspense 为基础建立路由级 Pending Boundary,并与流式渲染配合让共享布局先显示、慢数据完成后再逐步补齐。
[!info] related notes
- 所属 MOC:
- 前置概念: react-server-components、nextjs-layouts
- 并列概念:
- 易混淆概念:
- 关系笔记:
Next.js Loading 与 Streaming
一句话定义
loading.tsx 是 Next.js App Router 为某个 route segment 提供的路由级 Pending UI;它建立在 React Suspense 之上,让页面在数据尚未准备完成时先显示 fallback,并允许后续内容继续流式补齐。
基本模型
目录:
dashboard/
├── layout.tsx
├── loading.tsx
└── page.tsx
如果 page.tsx 里的数据获取很慢:
export default async function Page() {
const data = await slowQuery()
return <Dashboard data={data} />
}
用户可以先看到:
Dashboard Layout
├── Sidebar
├── Header
└── Loading Skeleton
等数据完成后再替换成真实 Page。
与 Suspense 的关系
可以先用这个心智模型理解:
loading.tsx
≈ route segment 的 Suspense fallback
也就是框架把 React 的 Suspense primitive 变成文件系统约定。
手工组件级 Suspense:
<Suspense fallback={<RevenueSkeleton />}>
<RevenueChart />
</Suspense>
Next.js 路由级:
loading.tsx
+ page.tsx
二者目标相同,但粒度不同。
Streaming
没有流式边界时,传统心智可能是:
等待所有数据
↓
完整页面
↓
一次性返回
有 Suspense / Streaming 后,可以变成:
Layout / Header / Sidebar
↓
先到达
Loading fallback
↓
先显示
慢数据完成
↓
真实内容继续流入
因此 Streaming 的价值不只是“服务器更快”,而是让快的内容不必被慢的内容阻塞。
Layout 为什么不会因为 Loading 消失
loading.tsx 主要替代的是其路由内容边界,而外层仍然有效的 Layout 可以继续保留。
AppLayout
├── Sidebar ← 保留
├── Header ← 保留
└── Route Content
├── Loading ← pending
└── Page ← ready 后替换
因此如果 Sidebar 的客户端状态位于被保留的 Layout 中,例如:
collapsed = true
子页面等待两秒并不会自动让它丢失。
Route-level 与 Component-level Loading
Route-level loading.tsx
适合:
- 整个页面主体都需要等待
- 希望快速给用户统一 Skeleton
- 路由切换时提供即时反馈
Component-level <Suspense>
适合:
- 只有局部数据慢
- 希望其他内容先完成
- 不想让整个页面都进入同一个 loading 状态
例如:
<DashboardSummary />
<Suspense fallback={<RevenueSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
这体现一个通用设计原则:
Boundary 应尽量贴近真正会慢的区域,而不是机械放得越高越好。
Route Group 可以控制 Loading Boundary 范围
如果:
dashboard/loading.tsx
影响范围太大,可以利用 Next.js Route Groups 重新组织:
dashboard/
├── (overview)/
│ ├── loading.tsx
│ └── page.tsx
├── tasks/page.tsx
└── settings/page.tsx
这样 URL 不变,但 route boundary 可以更精确。
Streaming 与 LLM Streaming 不是一回事
Next.js 页面 Streaming 主要讨论:
React/HTML/RSC UI 如何分段到达并逐步展示。
而 LLM Streaming 常讨论:
Token、事件或模型输出如何通过 SSE/WebSocket 等协议持续传输。
两者都叫 Streaming,但流的对象和协议层次不同,不应混为一谈。
常见误区
loading.tsx 不是“全局 Spinner 开关”
它属于 route segment boundary,有明确作用范围。
Streaming 不等于数据请求一定更快
慢查询仍然可能很慢;Streaming 优化的是等待组织方式和感知性能。
有 loading.tsx 以后仍然需要局部 Suspense
路由级 fallback 粒度较大,复杂页面经常同时使用更细的组件级 Suspense。
最短记忆方式
loading.tsx
= route-level pending boundary
+ Suspense fallback
+ streaming entry point
它主要回答:这一层内容还没准备好时,用户先看到什么?