N09第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

流式渲染与加载边界

流式渲染与加载边界 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

页面数据一多,慢查询就开始拖累整页。本课解决「一个慢块白屏全场」的问题:读完你能用 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。

示例拆解

在本机工程里走一遍完整链路:

  1. 给 getLessonStats 临时加一行 await new Promise((r) => setTimeout(r, 800)); 模拟慢查询。
  2. 打开课程详情页:骨架先出现,正文立即渲染,统计区先显示 fallback,800ms 后被真实数据替换。
  3. 让统计查询故意 throw:确认只有对应区块显示错误占位,正文照常可读。
  4. 删掉临时延迟,恢复代码。

关键点:

  • loading.tsx 管「路由切换期间」,Suspense 管「同一页内的先后」,error.tsx 管「坏了怎么办」——三者职责不同,互不替代。
  • fallback 要有信息量(标题骨架、行数占位),并贴近最终布局,否则内容到达时页面跳动。

动手任务

在本机工程里完成:

  1. 给课程详情页加 loading.tsx,导航进入时确认骨架立即出现。
  2. 把最慢的那个查询包进 Suspense,用 Network 面板确认 HTML 分两段到达。
  3. 给对应路由段加 error.tsx,制造一次 throw,验证正文仍可读、可重试。

常见错误

  • 在 Suspense 里包普通客户端组件:流式靠的是服务端 async 组件的 await,包一个不返回 Promise 的组件不会产生流式效果。
  • fallback 与正文布局差异太大:内容到达时整页跳动,看起来像 bug;骨架要模拟真实结构。
  • 以为 loading.tsx 能兜住错误:它只管加载中,错误要 error.tsx 处理,两者经常被混为一谈。
  • 把整页包进一个大 Suspense:等于没拆,慢的照样拖住全部;要按数据快慢拆边界。

检查与复盘

自查清单:

  • 课程页首屏正文不再等慢查询,DevTools 里能看到 HTML 分段到达
  • 每个慢区块都有独立的加载占位与失败回退
  • loading.tsx、Suspense、error.tsx 三者分工说得清楚

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码中等

阅读代码。SlowList 内部 await 查询数据库约需 2 秒。用户首次打开该页面,浏览器最先收到并显示的内容是?

import { Suspense } from 'react';

export default function Layout({ children }) {
  return (
    <main>
      <h1>课程目录</h1>
      <Suspense fallback={<p>加载中…</p>}>
        <SlowList />
      </Suspense>
      {children}
    </main>
  );
}
Q2单选简单

「流式渲染(streaming)」主要解决什么问题?

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...