流式渲染与加载边界
流式渲染与加载边界 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
页面数据一多,慢查询就开始拖累整页。本课解决「一个慢块白屏全场」的问题:读完你能用 loading.tsx 给路由配即时加载态,用 Suspense 把慢内容单独流式送出,并验证局部失败不会破坏整页使用。
问题场景
课程详情页有两块内容:课程正文(读本地文件,几毫秒)和「本课学习统计」(聚合查询,几百毫秒,开发时你还加了人工延迟模拟)。现在的写法是页面 await 完两个查询再一起返回——结果整页白屏 800ms,快的内容陪着慢的一起等。用户盯着空白页,不知道网站是卡死了还是在思考。
核心解释
流式渲染的思路:别把整个页面当成一次交付,让快内容先走,慢内容到了再补。Next.js App Router 给了三级工具。
loading.tsx:路由级加载骨架。 放在路由段目录里,导航发生的瞬间就显示,页面内容到达后自动替换:
// app/courses/[slug]/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 w-1/2 bg-neutral-200 rounded" />
<div className="h-4 w-full bg-neutral-100 rounded mt-4" />
</div>
);
}
Suspense:页面内分级。 同一页面里,把慢的 async 组件单独包住,框架先发出 fallback,组件完成后把真实内容流进同一份 HTML:
// app/courses/[slug]/page.tsx
import { Suspense } from "react";
export default async function CoursePage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const course = await getCourse(slug); // 快:正文直接渲染
return (
<article>
<h1>{course.title}</h1>
<CourseBody slug={slug} />
<Suspense fallback={<p>统计加载中……</p>}>
<LessonStats slug={slug} /> {/* 慢:单独流式送达 */}
</Suspense>
</article>
);
}
async function LessonStats({ slug }: { slug: string }) {
const stats = await getLessonStats(slug); // 慢查询
return <p>已学完 {stats.completed} 节,共 {stats.total} 节</p>;
}
错误边界跟着拆。 流式送出的内容如果抛错,整页 500 太重——给路由段配 error.tsx,局部失败只影响那一块:
// app/courses/[slug]/error.tsx
"use client";
export default function CourseError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<p>这一块加载失败了,页面其他部分不受影响。</p>
<button onClick={reset}>重试</button>
</div>
);
}
验证流式生效的方法:浏览器 DevTools 的 Network 面板点开文档请求,能看到 HTML 分段到达——先正文后统计。想要更细的失败粒度,可以把慢块挪进独立的子路由段,让它拥有自己的 error.tsx。
示例拆解
在本机工程里走一遍完整链路:
- 给
getLessonStats临时加一行await new Promise((r) => setTimeout(r, 800));模拟慢查询。 - 打开课程详情页:骨架先出现,正文立即渲染,统计区先显示 fallback,800ms 后被真实数据替换。
- 让统计查询故意 throw:确认只有对应区块显示错误占位,正文照常可读。
- 删掉临时延迟,恢复代码。
关键点:
- loading.tsx 管「路由切换期间」,Suspense 管「同一页内的先后」,error.tsx 管「坏了怎么办」——三者职责不同,互不替代。
- fallback 要有信息量(标题骨架、行数占位),并贴近最终布局,否则内容到达时页面跳动。
动手任务
在本机工程里完成:
- 给课程详情页加 loading.tsx,导航进入时确认骨架立即出现。
- 把最慢的那个查询包进 Suspense,用 Network 面板确认 HTML 分两段到达。
- 给对应路由段加 error.tsx,制造一次 throw,验证正文仍可读、可重试。
常见错误
- 在 Suspense 里包普通客户端组件:流式靠的是服务端 async 组件的 await,包一个不返回 Promise 的组件不会产生流式效果。
- fallback 与正文布局差异太大:内容到达时整页跳动,看起来像 bug;骨架要模拟真实结构。
- 以为 loading.tsx 能兜住错误:它只管加载中,错误要 error.tsx 处理,两者经常被混为一谈。
- 把整页包进一个大 Suspense:等于没拆,慢的照样拖住全部;要按数据快慢拆边界。
检查与复盘
自查清单:
- 课程页首屏正文不再等慢查询,DevTools 里能看到 HTML 分段到达
- 每个慢区块都有独立的加载占位与失败回退
- loading.tsx、Suspense、error.tsx 三者分工说得清楚
参考资料
- Next.js 文档:Loading UI and Streaming —— loading.tsx 与流式渲染的官方说明
- React 文档:Suspense —— Suspense 的行为细节与边界规则
- Next.js 文档:error 文件约定 —— 路由段错误边界的用法
读完这一课了?
标记完成后,工作台会安排接下来的学习。