Suspense 是 Next.js 项目中常用的一个组件,了解其原理和背景有助于我们正确使用 Suspense 组件。
传统 SSR
传统 SSR,需要经过一系列的步骤,用户才能查看页面、与之交互。
具体这些步骤是:
- 服务端获取所有数据;
- 服务端渲染 HTML;
- 将页面的 HTML、CSS、JavaScript 发送到客户端;
- 使用 HTML 和 CSS 生成不可交互的用户界面(non-interactive UI);
- React 对用户界面进行水合(hydrate),使其可交互(interactive UI);

这些步骤是连续的、阻塞的。这意味着服务端只能在获取所有数据后渲染 HTML,React 只能在下载了所有组件代码后才能进行水合:

传统 SSR 几个缺点如下:
- SSR 的数据获取必须在组件渲染之前
- 组件的 JavaScript 必须先加载到客户端,才能开始水合
- 所有组件必须先水合,然后才能跟其中任意一个组件交互;
Suspense
为了解决这些问题,React 18 引入了 <Suspense> 组件。
<Suspense> 允许你推迟渲染某些内容,直到满足某些条件(例如数据加载完毕)。
你可以将动态组件包装在 Suspense 中,然后向其传递一个 fallback UI,以便在动态组件加载时显示。
如果数据请求缓慢,使用 Suspense 流式渲染该组件,不会影响页面其他部分的渲染,更不会阻塞整个页面。
让我们来写一个例子,新建 app/dashboard/page.js,代码如下:
js
import { Suspense } from 'react'
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function PostFeed() {
await sleep(2000)
return <h1>Hello PostFeed</h1>
}
async function Weather() {
await sleep(8000)
return <h1>Hello Weather</h1>
}
async function Recommend() {
await sleep(5000)
return <h1>Hello Recommend</h1>
}
export default function Dashboard() {
return (
<section style={{padding: '20px'}}>
<Suspense fallback={<p>Loading PostFeed Component</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading Weather Component</p>}>
<Weather />
</Suspense>
<Suspense fallback={<p>Loading Recommend Component</p>}>
<Recommend />
</Suspense>
</section>
)
}
在这个例子中,我们用 Suspense 包装了三个组件,并通过 sleep 函数模拟了数据请求耗费的时长。加载效果如下:

让我们观察下 dashboard 这个 HTML 文件的加载情况,你会发现它一开始是 2.03s,然后变成了 5.03s,最后变成了 8.04s,这不就正是我们设置的 sleep 时间吗?
查看 dashboard 请求的响应头:

Transfer-Encoding 标头的值为 chunked,表示数据将以一系列分块的形式进行发送。
分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制,允许 HTTP 由网页服务器发送给客户端应用( 通常是网页浏览器)的数据可以分成多个部分。分块传输编码只在 HTTP 协议1.1版本(HTTP/1.1)中提供。
再查看 dashboard 返回的数据(这里我们做了简化):
js
<!DOCTYPE html>
<html lang="en">
<head>
// ...
</head>
<body class="__className_aaf875">
<section style="padding:20px">
<!--$?-->
<template id="B:0"></template>
<p>Loading PostFeed Component</p>
<!--/$-->
<!--$?-->
<template id="B:1"></template>
<p>Loading Weather Component</p>
<!--/$-->
<!--$?-->
<template id="B:2"></template>
<p>Loading Recommend Component</p>
<!--/$-->
</section>
// ...
<div hidden id="S:0">
<h1>Hello PostFeed</h1>
</div>
<script>
// 交换位置
$RC = function(b, c, e) {
// ...
};
$RC("B:0", "S:0")
</script>
<div hidden id="S:2">
<h1>Hello Recommend</h1>
</div>
<script>
$RC("B:2", "S:2")
</script>
<div hidden id="S:1">
<h1>Hello Weather</h1>
</div>
<script>
$RC("B:1", "S:1")
</script>
</body>
</html>
可以看到使用 Suspense 组件的 fallback UI 和渲染后的内容都会出现在该 HTML 文件中,说明该请求持续与服务端保持连接,服务端在组件渲染完后会将渲染后的内容追加传给客户端。
客户端收到新的内容后进行解析,执行类似于 $RC("B:2", "S:2")这样的函数交换 DOM 内容,使 fallback UI 替换为渲染后的内容。
这个过程被称之为 Streaming Server Rendering(流式渲染) ,它解决了上节说的传统 SSR 的第一个问题,那就是数据获取必须在组件渲染之前。使用 Suspense,先渲染 Fallback UI,等数据返回再渲染具体的组件内容。
使用 Suspense 还有一个好处就是 Selective Hydration(选择性水合)。简单的来说,当多个组件等待水合的时候,React 可以根据用户交互决定组件水合的优先级。比如 Sidebar 和 MainContent 组件都在等待水合,快要到 Sidebar 了,但此时用户点击了 MainContent 组件,React 会在单击事件的捕获阶段同步水合 MainContent 组件以保证立即响应,Sidebar 稍后水合。
Streaming
Suspense 背后的这种技术称之为 Streaming。
将页面的 HTML 拆分成多个 chunks,然后逐步将这些块从服务端发送到客户端。

简单理解:
- 核心逻辑 :服务器在生成 HTML 时,不用等所有数据(比如接口请求、数据库查询)都返回,而是先把页面的静态骨架(导航、页脚、空的内容容器)以 chunk 块的形式发给浏览器,浏览器收到后立刻渲染出骨架,减少白屏时间。
- 之后服务器再逐个获取动态数据(比如商品列表、文章内容),每拿到一部分就生成对应的 HTML 片段,再以 chunk 块发送给浏览器,浏览器接收到后增量插入到页面中 ,页面内容逐步 填充 完成。
- 技术依赖 :就是我们之前说的 HTTP 分块传输(
Transfer-Encoding: chunked)或 HTTP/2 二进制分帧,Next.js、Nuxt.js 等框架的 SSR 流式渲染都是这么实现的。
这样就可以更快的展现出页面的某些内容,而无需在渲染 UI 之前等待加载所有数据。
提前发送的组件可以提前开始水合,这样当其他部分还在加载的时候,用户可以和已完成水合的组件进行交互,有效改善用户体验。
传统 SSR:

使用 Streaming 后:

在 Next.js 中有两种实现 Streaming 的方法:
- 页面级别,使用
loading.jsx - 特定组件,使用
<Suspense>
Suspense 和 Streaming 确实很好,将原本只能先获取数据、再渲染水合的传统 SSR 改为渐进式渲染水合。
但还有一些问题没有解决:
1. 用户下载的 JavaScript 代码,该下载的代码还是没有少,可是用户真的需要下载那么多的 Javascript 代码吗?
2. 所有的组件都必须在客户端进行水合,对于不需要交互性的组件其实没有必要进行水合。
要解决这个问题,就是服务端组件要解决的问题了。
