首页 / Next.js 16 入门教程 / 加载态与流式渲染

Next.js 16 入门教程

加载态与流式渲染

本教程共 42 篇 · 第 14 篇 · 更新于 2026-07-30 · 约 8 分钟阅读

Next.jsNext.js 16 入门教程loading.tsxSuspense流式渲染SSR

14. 加载态与流式渲染

本节目标:学会用 loading.tsx 和 Suspense 创建加载态,理解流式 SSR 的工作原理,掌握提升感知性能的技巧。

什么是流式渲染

传统 SSR 要等所有数据加载完才发送 HTML。一个慢查询就能卡住整个页面。

流式渲染改变了这一点——服务器先生成能生成的部分,立即发送给客户端。数据就绪的部分陆续填入。用户看到页面在”渐进式”出现。

传统 SSR:[等待...等待...等待...] -> 一次性显示全部
流式渲染:[立即显示骨架] -> [填入头部] -> [填入内容] -> [填入推荐]

loading.tsx

最简单的加载态:在路由文件夹里放一个 loading.tsx

app/
└── dashboard/
    ├── loading.tsx    # 加载态
    ├── page.tsx       # 实际页面
    └── layout.tsx     # 布局
// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-8 w-48 bg-gray-200 rounded mb-4" />
      <div className="h-4 w-full bg-gray-200 rounded mb-2" />
      <div className="h-4 w-full bg-gray-200 rounded mb-2" />
      <div className="h-4 w-2/3 bg-gray-200 rounded" />
    </div>
  )
}

Next.js 会自动把 page.tsx 包裹在 <Suspense> 里,用 loading.tsx 作为 fallback。

工作原理

layout.tsx(立即渲染)
  └── loading.tsx(立即显示)
      └── page.tsx(数据加载完后替换 loading)

导航时:

  1. 布局立即显示
  2. 骨架屏立即显示
  3. 页面数据加载完后,替换骨架屏

预取

loading.tsx 的 fallback 会被预取。用户点击链接时,骨架屏立即出现——即使数据还在加载。这让导航感觉是即时的。

Suspense 组件

loading.tsx 适合整页加载态。更细粒度的控制用 <Suspense>

import { Suspense } from 'react'

export default function Page() {
  return (
    <div>
      <header>
        <h1>仪表盘</h1>
      </header>
      <main>
        <Suspense fallback={<p>加载收入...</p>}>
          <Revenue />
        </Suspense>
        <Suspense fallback={<p>加载订单...</p>}>
          <RecentOrders />
        </Suspense>
      </main>
    </div>
  )
}

每个 <Suspense> 独立加载,互不阻塞。

并行加载

多个 <Suspense> 同时加载,谁先完成谁先显示:

export default function Dashboard() {
  return (
    <div>
      <h1>仪表盘</h1>
      <div className="grid grid-cols-2 gap-4">
        <Suspense fallback={<Skeleton />}>
          <Revenue />      {/* 200ms 完成 */}
        </Suspense>
        <Suspense fallback={<Skeleton />}>
          <RecentOrders /> {/* 1s 完成 */}
        </Suspense>
      </div>
      <Suspense fallback={<Skeleton />}>
        <Recommendations /> {/* 3s 完成 */}
      </Suspense>
    </div>
  )
}

用户先看到收入模块,然后订单,最后推荐——不用等最慢的。

嵌套 Suspense

可以创建分层加载体验:

export default async function ProductPage({ params }) {
  const { id } = await params

  return (
    <div>
      <h1>产品详情</h1>
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetails id={id} />
        <Suspense fallback={<ReviewsSkeleton />}>
          <Reviews productId={id} />
        </Suspense>
      </Suspense>
    </div>
  )
}

外层加载完显示产品详情,内层继续加载评论。

loading.tsx vs Suspense

loading.tsx<Suspense>
范围整页任意组件
设置放文件包裹组件
预取自动预取默认不预取
适用场景页面没数据就没内容大部分页面

推荐:优先用 <Suspense>,把 fallback 放在需要它的组件旁边。

流式渲染的两个流

HTML 流

React 服务端渲染器生成渐进式 HTML 块:

  1. 静态部分(布局、导航、fallback)先发送
  2. 异步组件完成后,HTML 和内联 <script> 一起发送
  3. 浏览器立即执行替换,无需等 JS 包加载

组件负载(RSC Payload)

组件负载是组件树的序列化表示,用于:

  • 首次加载:注水(hydrate)页面
  • 客户端导航:只获取 RSC Payload,不传 HTML

静态 shell

在任何异步工作之前渲染的内容叫静态 shell

  • 布局
  • 导航
  • Suspense fallback
  • 缓存的组件(use cache

静态 shell 立即发送给用户,提供即时视觉反馈。

把动态访问往下推

最大化静态 shell 的关键:把动态数据访问推迟到真正需要的组件。

// ❌ 不好:layout 里 await cookies,整个布局变成动态
export default async function Layout({ children }) {
  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')?.value

  return (
    <div data-theme={theme}>
      <nav>...</nav>
      {children}
    </div>
  )
}

// ✅ 好:只把需要 cookies 的部分包在 Suspense 里
export default function Layout({ children }) {
  const cookieStore = cookies() // 不 await

  return (
    <div>
      <nav>...</nav>
      <Suspense fallback={<div>加载中...</div>}>
        <UserMenu cookiePromise={cookieStore} />
      </Suspense>
      {children}
    </div>
  )
}

同样的原则适用于 paramssearchParams 和数据获取。

设计有意义的加载态

好的加载态不是转圈圈,而是让用户知道:

  1. 应用正在响应
  2. 即将显示什么内容
  3. 大概还要多久

Skeleton 模式

骨架屏模拟最终内容的形状:

export default function CardSkeleton() {
  return (
    <div className="animate-pulse rounded border p-4">
      <div className="h-48 bg-gray-200 rounded mb-4" />
      <div className="h-6 w-3/4 bg-gray-200 rounded mb-2" />
      <div className="h-4 w-full bg-gray-200 rounded mb-2" />
      <div className="h-4 w-2/3 bg-gray-200 rounded" />
    </div>
  )
}

关键:尺寸匹配

骨架屏的尺寸应该和最终内容一致,避免布局抖动(CLS)。

流式渲染与 Web Vitals

TTFB(首字节时间)

流式渲染让 TTFB 大幅降低——服务器一有静态 shell 就发送,不用等数据。

FCP(首次内容绘制)

静态 shell 立即渲染,FCP 和数据获取时间解耦。

LCP(最大内容绘制)

如果 LCP 元素在 Suspense 边界内,它要等数据加载完才显示。保持 LCP 元素在边界外。

CLS(累积布局偏移)

骨架屏和最终内容尺寸不一致会导致布局偏移。用固定高度或 min-height 预留空间。

HTTP 契约

流式渲染开始后,HTTP 响应头(包括状态码)已经发送。无法再修改状态码

  • notFound() 在流式渲染中:注入 <meta name="robots" content="noindex">
  • redirect() 在流式渲染中:变成客户端跳转

要获取真正的 404 状态码,在流式渲染开始前检查:

export default async function PostPage({ params }) {
  const { slug } = await params
  const exists = await checkSlugExists(slug)

  if (!exists) notFound() // 在 Suspense 之前

  return (
    <Suspense fallback={<p>加载中...</p>}>
      <PostContent slug={slug} />
    </Suspense>
  )
}

影响流式渲染的因素

  • 反向代理:Nginx 默认缓冲,需设置 X-Accel-Buffering: no
  • CDN:部分 CDN 缓冲整个响应
  • Serverless:AWS Lambda 需开启 response streaming
  • 压缩:Gzip/Brotli 可能缓冲
  • Safari:缓冲前 1024 字节

小结

这一章我们学了加载态和流式渲染:

  1. loading.tsx 创建整页加载态,自动预取
  2. <Suspense> 创建细粒度加载态
  3. 流式渲染让页面分段显示,提升感知性能
  4. 静态 shell 包含布局、导航、fallback
  5. 把动态访问往下推,最大化静态 shell
  6. 骨架屏尺寸匹配,避免布局偏移

恭喜!你已经完成了基础入门部分的学习。接下来可以继续学习核心进阶章节。