Appearance
第8章 流式SSR(Fizz)
本章将深入React 19的流式服务端渲染引擎Fizz,理解Task任务系统、Segment分段渲染、Suspense在SSR中的处理机制。这是理解React全栈架构的关键一章。
在传统的服务端渲染(SSR)中,服务器必须等待整个页面渲染完成后,才能将HTML发送给客户端。如果页面中有慢速的数据获取操作,用户就需要长时间等待白屏。React 18引入了流式SSR,允许服务器边渲染边发送HTML,大大提升了首屏加载速度和用户体验。
为什么需要流式SSR?Fizz是如何实现流式渲染的?Task任务系统如何调度渲染工作?Segment如何分段输出HTML?Suspense在SSR中如何处理异步组件?
本章将逐一解答这些问题,带你深入理解React流式SSR的设计与实现。
8.1 Fizz架构概述
Fizz是React服务端流式渲染引擎的内部代号,它是React 18+服务端渲染的核心实现。
8.1.1 什么是Fizz
Fizz是React的服务端渲染器,负责将React组件树渲染成HTML字符串并通过流式输出发送给客户端。
Fizz的核心特性
- 流式输出:边渲染边发送,不需要等待整个页面完成
- Suspense支持:可以延迟渲染慢速组件,先发送fallback
- 选择性水合:客户端可以优先水合用户交互的部分
- 错误恢复:支持错误边界,局部错误不影响整体渲染
为什么叫Fizz?
Fizz这个名字来源于"fizzy"(起泡的),形象地描述了HTML像气泡一样不断从服务器"冒出"并发送给客户端的过程。
8.1.2 Fizz与传统SSR的区别
让我们通过对比来理解Fizz的优势。
传统SSR的问题
jsx
// 传统SSR流程
async function traditionalSSR(App) {
// 1. 等待所有数据加载
const data1 = await fetchSlowData1(); // 3秒
const data2 = await fetchSlowData2(); // 2秒
// 2. 渲染整个页面
const html = renderToString(<App data1={data1} data2={data2} />);
// 3. 发送完整HTML(总共5秒后)
res.send(html);
}
// 用户体验:
// 0-5秒:白屏等待
// 5秒:看到完整页面Fizz流式SSR的优势
jsx
// Fizz流式SSR流程
async function streamingSSR(App) {
const stream = renderToPipeableStream(<App />, {
onShellReady() {
// 1. 立即发送页面骨架(0.1秒)
stream.pipe(res);
}
});
}
// 组件中使用Suspense
function App() {
return (
<div>
<Header /> {/* 立即渲染 */}
<Suspense fallback={<Spinner />}>
<SlowComponent1 /> {/* 3秒后渲染 */}
</Suspense>
<Suspense fallback={<Spinner />}>
<SlowComponent2 /> {/* 2秒后渲染 */}
</Suspense>
</div>
);
}
// 用户体验:
// 0.1秒:看到Header和两个Spinner
// 2秒:SlowComponent2替换Spinner
// 3秒:SlowComponent1替换Spinner对比表格
| 特性 | 传统SSR | Fizz流式SSR |
|---|---|---|
| 首字节时间(TTFB) | 慢(等待所有数据) | 快(立即发送骨架) |
| 用户感知 | 长时间白屏 | 渐进式加载 |
| Suspense支持 | 不支持 | 完全支持 |
| 错误处理 | 全局失败 | 局部降级 |
| 选择性水合 | 不支持 | 支持 |
8.1.3 核心文件结构
Fizz的核心实现位于packages/react-server/src/目录下。
主要文件
packages/react-server/src/
├── ReactFizzServer.js # Fizz核心入口
├── ReactFizzThenable.js # Promise/Thenable处理
├── ReactFizzCache.js # 服务端缓存
├── ReactFizzHooks.js # 服务端Hooks实现
├── ReactFizzComponentStack.js # 组件栈追踪
└── forks/
├── ReactFizzConfig.dom-node.js # Node.js环境配置
├── ReactFizzConfig.dom-browser.js # 浏览器环境配置
└── ReactFizzConfig.dom-edge.js # Edge环境配置ReactFizzServer.js的核心导出
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// Node.js环境的流式渲染
export function renderToPipeableStream(
children: ReactNodeList,
options?: Options,
): PipeableStream;
// Web Streams API的流式渲染
export function renderToReadableStream(
children: ReactNodeList,
options?: Options,
): Promise<ReadableStream>;
// 静态HTML生成(不支持Suspense)
export function renderToStaticMarkup(
children: ReactNodeList,
options?: Options,
): string;核心数据结构
javascript
// Request:渲染请求对象
type Request = {
destination: Destination, // 输出目标(流)
allPendingTasks: number, // 待处理任务数
pendingRootTasks: number, // 根任务数
completedRootSegment: null | Segment, // 完成的根Segment
abortableTasks: Set<Task>, // 可中止的任务集合
pingedTasks: Array<Task>, // 被唤醒的任务
// ... 其他属性
};
// Task:渲染任务
type Task = {
node: ReactNodeList, // 要渲染的React节点
ping: () => void, // 唤醒任务的函数
blockedBoundary: null | SuspenseBoundary, // 阻塞的Suspense边界
blockedSegment: Segment, // 阻塞的Segment
// ... 其他属性
};
// Segment:HTML片段
type Segment = {
status: SegmentStatus, // 状态:PENDING/COMPLETED/ERRORED
id: number, // Segment ID
chunks: Array<Chunk>, // HTML块数组
children: Array<Segment>, // 子Segment
// ... 其他属性
};Fizz的工作流程
8.2 renderToPipeableStream源码分析
renderToPipeableStream是Node.js环境下流式SSR的入口函数,它返回一个可管道化的流对象。
8.2.1 函数入口与参数
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:2850-2900(React 19.3.0)
export function renderToPipeableStream(
children: ReactNodeList,
options?: Options,
): PipeableStream {
// 1. 创建Request对象
const request = createRequest(
children,
null, // responseState
createRootFormatContext(),
Infinity, // progressiveChunkSize
options ? options.onError : undefined,
options ? options.onAllReady : undefined,
options ? options.onShellReady : undefined,
options ? options.onShellError : undefined,
undefined, // onFatalError
options ? options.onPostpone : undefined,
options ? options.formState : undefined,
);
// 2. 标记为已开始
let hasStartedFlowing = false;
// 3. 开始异步渲染
startWork(request);
// 4. 返回PipeableStream对象
return {
pipe<T: Writable>(destination: T): T {
if (hasStartedFlowing) {
throw new Error(
'React currently only supports piping to one writable stream.'
);
}
hasStartedFlowing = true;
startFlowing(request, destination);
destination.on('drain', createDrainHandler(destination, request));
return destination;
},
abort(reason: mixed) {
abort(request, reason);
},
};
}参数说明
- children:要渲染的React元素
- options:配置选项
- onError:错误处理回调
- onAllReady:所有内容就绪回调
- onShellReady:Shell就绪回调(推荐使用)
- onShellError:Shell错误回调
- onPostpone:延迟渲染回调
- formState:表单状态(Server Actions)
返回值:PipeableStream
javascript
type PipeableStream = {
// 将渲染结果管道到可写流
pipe<T: Writable>(destination: T): T,
// 中止渲染
abort(reason: mixed): void,
};8.2.2 Request对象创建
Request对象是Fizz渲染的核心,它保存了渲染的所有状态。
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1150-1250(React 19.3.0,简化版)
function createRequest(
children: ReactNodeList,
responseState: ResponseState,
rootFormatContext: FormatContext,
progressiveChunkSize: number,
onError: void | ((error: mixed) => ?string),
onAllReady: void | (() => void),
onShellReady: void | (() => void),
onShellError: void | ((error: mixed) => void),
onFatalError: void | ((error: mixed) => void),
onPostpone: void | ((reason: string) => void),
formState: void | null | FormState,
): Request {
// 1. 创建根Segment
const pingedTasks: Array<Task> = [];
const abortSet: Set<Task> = new Set();
const request: Request = {
destination: null,
flushScheduled: false,
resumableState: createResumableState(
responseState ? responseState.identifierPrefix : undefined,
responseState ? responseState.unstable_externalRuntimeSrc : undefined,
),
responseState,
progressiveChunkSize:
progressiveChunkSize === undefined ? 12800 : progressiveChunkSize,
status: OPENING,
fatalError: null,
nextSegmentId: 0,
allPendingTasks: 0,
pendingRootTasks: 0,
completedRootSegment: null,
abortableTasks: abortSet,
pingedTasks: pingedTasks,
clientRenderedBoundaries: ([]: Array<SuspenseBoundary>),
completedBoundaries: ([]: Array<SuspenseBoundary>),
partialBoundaries: ([]: Array<SuspenseBoundary>),
trackedPostpones: null,
onError: onError === undefined ? defaultOnError : onError,
onPostpone: onPostpone === undefined ? defaultOnPostpone : onPostpone,
onAllReady: onAllReady === undefined ? noop : onAllReady,
onShellReady: onShellReady === undefined ? noop : onShellReady,
onShellError: onShellError === undefined ? noop : onShellError,
onFatalError: onFatalError === undefined ? noop : onFatalError,
formState: formState === undefined ? null : formState,
};
// 2. 创建根Segment
const rootSegment = createPendingSegment(
request,
0,
null,
rootFormatContext,
false,
false,
);
rootSegment.parentFlushed = true;
// 3. 创建根Task
const rootTask = createRenderTask(
request,
null,
children,
-1,
null,
rootSegment,
abortSet,
null,
rootFormatContext,
emptyContextObject,
null,
emptyTreeContext,
);
pingedTasks.push(rootTask);
return request;
}Request对象的关键属性
- destination:输出目标(Node.js的Writable流)
- status:渲染状态
- OPENING:正在渲染Shell
- CLOSING:Shell完成,渲染延迟内容
- CLOSED:全部完成
- allPendingTasks:待处理任务总数
- pendingRootTasks:根任务数(Shell相关)
- completedRootSegment:完成的根Segment
- pingedTasks:被唤醒的任务队列
- completedBoundaries:完成的Suspense边界
8.2.3 流式输出机制
Fizz通过Node.js的Stream API实现流式输出。
startFlowing:开始流式输出
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
function startFlowing(request: Request, destination: Destination): void {
// 1. 设置destination
request.destination = destination;
// 2. 尝试完成请求
try {
flushCompletedQueues(request, destination);
} catch (error) {
logRecoverableError(request, error);
fatalError(request, error);
}
}flushCompletedQueues:刷新完成的队列
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:2200-2300(React 19.3.0,简化版)
function flushCompletedQueues(
request: Request,
destination: Destination,
): void {
beginWriting(destination);
try {
// 1. 刷新完成的根Segment
const completedRootSegment = request.completedRootSegment;
if (completedRootSegment !== null && request.pendingRootTasks === 0) {
flushSegment(request, destination, completedRootSegment);
request.completedRootSegment = null;
writeBootstrap(destination, request.resumableState);
}
// 2. 刷新完成的Suspense边界
const completedBoundaries = request.completedBoundaries;
let i = 0;
for (; i < completedBoundaries.length; i++) {
const boundary = completedBoundaries[i];
if (!flushCompletedBoundary(request, destination, boundary)) {
// 流已满,暂停刷新
request.destination = null;
i++;
completedBoundaries.splice(0, i);
return;
}
}
completedBoundaries.splice(0, i);
// 3. 刷新部分边界
const partialBoundaries = request.partialBoundaries;
i = 0;
for (; i < partialBoundaries.length; i++) {
const boundary = partialBoundaries[i];
if (!flushPartialBoundary(request, destination, boundary)) {
request.destination = null;
i++;
partialBoundaries.splice(0, i);
return;
}
}
partialBoundaries.splice(0, i);
// 4. 刷新客户端渲染的边界
const clientRenderedBoundaries = request.clientRenderedBoundaries;
i = 0;
for (; i < clientRenderedBoundaries.length; i++) {
const boundary = clientRenderedBoundaries[i];
if (!flushClientRenderedBoundary(request, destination, boundary)) {
request.destination = null;
i++;
clientRenderedBoundaries.splice(0, i);
return;
}
}
clientRenderedBoundaries.splice(0, i);
} finally {
completeWriting(destination);
// 5. 检查是否全部完成
if (
request.allPendingTasks === 0 &&
request.pingedTasks.length === 0 &&
request.clientRenderedBoundaries.length === 0 &&
request.completedBoundaries.length === 0
) {
close(destination);
}
}
}输出流程图
示例:使用renderToPipeableStream
javascript
// server.js
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';
app.get('/', (req, res) => {
const { pipe, abort } = renderToPipeableStream(<App />, {
// Shell就绪时调用(推荐)
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
// Shell错误时调用
onShellError(error) {
res.statusCode = 500;
res.send('<h1>Server Error</h1>');
},
// 所有内容就绪时调用
onAllReady() {
console.log('All content ready');
},
// 错误处理
onError(error) {
console.error('Render error:', error);
},
});
// 请求超时时中止渲染
req.setTimeout(10000, () => {
abort();
});
});Shell vs All Ready
jsx
function App() {
return (
<html>
<head>
<title>My App</title>
</head>
<body>
<Header /> {/* Shell的一部分 */}
<Suspense fallback={<Spinner />}>
<SlowContent /> {/* 不是Shell的一部分 */}
</Suspense>
<Footer /> {/* Shell的一部分 */}
</body>
</html>
);
}
// 时间线:
// 0.1秒:onShellReady触发
// - 发送:<Header /> + <Spinner /> + <Footer />
// 3秒: SlowContent完成
// - 发送:<SlowContent />的HTML + 替换脚本
// 3秒: onAllReady触发Shell的定义
Shell是指不在Suspense边界内的所有内容,包括:
- HTML结构(html、head、body标签)
- 不在Suspense内的组件
- Suspense的fallback
为什么使用onShellReady而不是onAllReady?
- 更快的首字节时间:Shell通常很快就能渲染完成
- 更好的用户体验:用户立即看到页面结构
- SEO友好:搜索引擎可以立即抓取Shell内容
- 渐进式加载:延迟内容在后台继续加载
javascript
// ✗ 不推荐:等待所有内容
onAllReady() {
pipe(res);
}
// 问题:如果有慢速组件,用户需要等待很久
// ✓ 推荐:Shell就绪时立即发送
onShellReady() {
pipe(res);
}
// 优势:用户立即看到页面,慢速内容渐进加载8.3 Task任务系统
Fizz使用Task来组织渲染工作,每个Task负责渲染一部分React节点。
8.3.1 Task数据结构
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:350-400(React 19.3.0)
type Task = {
node: ReactNodeList, // 要渲染的React节点
ping: () => void, // 唤醒任务的函数
blockedBoundary: null | SuspenseBoundary, // 阻塞的Suspense边界
blockedSegment: Segment, // 阻塞的Segment
abortSet: Set<Task>, // 可中止任务集合
keyPath: KeyNode, // 组件key路径
formatContext: FormatContext, // 格式上下文(HTML/SVG)
legacyContext: LegacyContext, // 旧版Context
context: ContextSnapshot, // 新版Context快照
treeContext: TreeContext, // 树上下文
componentStack: null | ComponentStackNode, // 组件栈
thenableState: null | ThenableState, // Promise状态
};Task的关键属性
node:要渲染的React节点
- 可以是元素、组件、文本等
- 渲染过程中会递归处理子节点
ping:唤醒函数
- 当异步操作完成时调用
- 将Task重新加入pingedTasks队列
blockedBoundary:阻塞的Suspense边界
- 如果Task在Suspense内,指向该Suspense
- 用于处理Suspense的fallback和内容
blockedSegment:阻塞的Segment
- Task渲染的目标Segment
- 渲染结果会写入这个Segment
abortSet:可中止任务集合
- 用于批量中止相关任务
- 当请求中止时,所有任务都会被中止
8.3.2 任务队列管理
Fizz维护多个任务队列来管理渲染工作。
任务队列类型
javascript
type Request = {
// 1. 被唤醒的任务(优先处理)
pingedTasks: Array<Task>,
// 2. 可中止的任务集合
abortableTasks: Set<Task>,
// 3. 待处理任务计数
allPendingTasks: number, // 所有待处理任务
pendingRootTasks: number, // Shell相关的根任务
};createRenderTask:创建渲染任务
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1300-1350(React 19.3.0,简化版)
function createRenderTask(
request: Request,
thenableState: ThenableState | null,
node: ReactNodeList,
childIndex: number,
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment,
abortSet: Set<Task>,
keyPath: KeyNode,
formatContext: FormatContext,
legacyContext: LegacyContext,
context: ContextSnapshot,
treeContext: TreeContext,
): Task {
// 1. 增加待处理任务计数
request.allPendingTasks++;
if (blockedBoundary === null) {
// 根任务(Shell的一部分)
request.pendingRootTasks++;
} else {
// Suspense内的任务
blockedBoundary.pendingTasks++;
}
// 2. 创建Task对象
const task: Task = {
replay: null,
node,
ping: () => pingTask(request, task),
blockedBoundary,
blockedSegment,
abortSet,
keyPath,
formatContext,
legacyContext,
context,
treeContext,
componentStack: null,
thenableState,
};
// 3. 添加到可中止任务集合
abortSet.add(task);
return task;
}pingTask:唤醒任务
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
function pingTask(request: Request, task: Task): void {
// 1. 将任务加入pingedTasks队列
const pingedTasks = request.pingedTasks;
pingedTasks.push(task);
// 2. 如果只有一个任务,立即处理
if (pingedTasks.length === 1) {
scheduleWork(() => performWork(request));
}
}retryTask:重试任务
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1800-1900(React 19.3.0,简化版)
function retryTask(request: Request, task: Task): void {
const segment = task.blockedSegment;
if (segment.status !== PENDING) {
// Segment已经完成或出错,不需要重试
return;
}
// 切换到Task的上下文
switchContext(task.context);
let prevTaskInDEV = null;
if (__DEV__) {
prevTaskInDEV = currentTaskInDEV;
currentTaskInDEV = task;
}
try {
// 重新渲染节点
renderNodeDestructive(request, task, task.node, -1);
// 渲染成功,完成Segment
segment.status = COMPLETED;
finishedTask(request, task.blockedBoundary, segment);
} catch (thrownValue) {
// 处理错误或Suspense
handleTaskError(request, task, thrownValue);
} finally {
if (__DEV__) {
currentTaskInDEV = prevTaskInDEV;
}
}
}8.3.3 任务执行流程
Fizz通过performWork函数驱动任务执行。
performWork:执行工作
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1700-1800(React 19.3.0,简化版)
function performWork(request: Request): void {
if (request.status === CLOSED) {
return;
}
const prevContext = getActiveContext();
const prevDispatcher = ReactSharedInternals.H;
ReactSharedInternals.H = HooksDispatcher;
try {
const pingedTasks = request.pingedTasks;
let i = 0;
// 处理所有被唤醒的任务
for (; i < pingedTasks.length; i++) {
const task = pingedTasks[i];
retryTask(request, task);
}
// 清空已处理的任务
pingedTasks.splice(0, i);
// 如果有destination,尝试刷新
if (request.destination !== null) {
flushCompletedQueues(request, request.destination);
}
} catch (error) {
logRecoverableError(request, error);
fatalError(request, error);
} finally {
ReactSharedInternals.H = prevDispatcher;
switchContext(prevContext);
}
}startWork:开始工作
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
function startWork(request: Request): void {
scheduleWork(() => performWork(request));
}
function scheduleWork(callback: () => void): void {
// 使用setImmediate或setTimeout(0)异步执行
setImmediate(callback);
}任务执行流程图
示例:任务执行过程
jsx
function App() {
return (
<div>
<Header />
<Suspense fallback={<Spinner />}>
<AsyncContent />
</Suspense>
<Footer />
</div>
);
}
// 任务执行过程:
// 1. 创建根Task
// - node: <App />
// - blockedBoundary: null(根任务)
// - blockedSegment: rootSegment
// 2. 执行根Task
// - 渲染<div>
// - 渲染<Header />
// - 遇到<Suspense>,创建Boundary
// - 渲染<Spinner />(fallback)
// - 创建延迟Task渲染<AsyncContent />
// - 渲染<Footer />
// - 根Task完成
// 3. Shell完成,触发onShellReady
// 4. <AsyncContent />的Promise resolve
// - 调用pingTask
// - 将延迟Task加入pingedTasks
// 5. 执行延迟Task
// - 渲染<AsyncContent />
// - 完成Boundary
// - 刷新Boundary的HTML
// 6. 所有Task完成,触发onAllReady8.4 Segment分段渲染
Segment是Fizz中HTML片段的基本单位,每个Segment对应一段可以独立输出的HTML。
8.4.1 Segment数据结构
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:250-300(React 19.3.0)
type Segment = {
status: SegmentStatus, // 状态
id: number, // Segment ID(用于客户端引用)
index: number, // 在父Segment中的索引
parentFlushed: boolean, // 父Segment是否已刷新
chunks: Array<Chunk>, // HTML块数组
children: Array<Segment>, // 子Segment
boundary: null | SuspenseBoundary, // 所属的Suspense边界
lastPushedText: string, // 最后推送的文本(用于合并)
textEmbedded: boolean, // 文本是否已嵌入
};
// Segment状态
const PENDING = 0; // 待处理
const COMPLETED = 1; // 已完成
const FLUSHED = 2; // 已刷新
const ABORTED = 3; // 已中止
const ERRORED = 4; // 出错Segment的关键属性
status:Segment的状态
- PENDING:正在渲染
- COMPLETED:渲染完成,等待刷新
- FLUSHED:已刷新到客户端
- ERRORED:渲染出错
id:Segment的唯一标识
- 用于客户端引用和替换
- 在Suspense场景中,客户端需要知道替换哪个Segment
chunks:HTML块数组
- 渲染过程中生成的HTML片段
- 最终会拼接成完整的HTML字符串
parentFlushed:父Segment是否已刷新
- 只有父Segment刷新后,子Segment才能刷新
- 保证HTML的顺序正确
8.4.2 分段的创建与完成
createPendingSegment:创建Segment
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:800-850(React 19.3.0)
function createPendingSegment(
request: Request,
index: number,
boundary: null | SuspenseBoundary,
parentFormatContext: FormatContext,
lastPushedText: boolean,
textEmbedded: boolean,
): Segment {
return {
status: PENDING,
id: -1, // 延迟分配ID
index,
parentFlushed: false,
chunks: [],
children: [],
boundary,
lastPushedText,
textEmbedded,
};
}finishedTask:完成任务
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1400-1500(React 19.3.0,简化版)
function finishedTask(
request: Request,
boundary: Root | SuspenseBoundary,
segment: Segment,
): void {
// 1. 减少待处理任务计数
request.allPendingTasks--;
if (boundary === null) {
// 根任务完成
request.pendingRootTasks--;
if (request.pendingRootTasks === 0) {
// 所有根任务完成,Shell就绪
request.completedRootSegment = segment;
if (request.destination !== null) {
flushCompletedQueues(request, request.destination);
}
}
} else {
// Suspense内的任务完成
boundary.pendingTasks--;
if (boundary.pendingTasks === 0) {
// Boundary的所有任务完成
if (boundary.status === PENDING) {
boundary.status = COMPLETED;
}
// 将Boundary加入完成队列
request.completedBoundaries.push(boundary);
if (request.destination !== null) {
flushCompletedQueues(request, request.destination);
}
}
}
}flushSegment:刷新Segment
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:2000-2100(React 19.3.0,简化版)
function flushSegment(
request: Request,
destination: Destination,
segment: Segment,
): boolean {
const boundary = segment.boundary;
if (boundary === null) {
// 根Segment,直接输出
return flushSegmentContainer(request, destination, segment);
} else {
// Suspense内的Segment,需要特殊处理
return flushSegmentContainer(request, destination, segment);
}
}
function flushSegmentContainer(
request: Request,
destination: Destination,
segment: Segment,
): boolean {
// 1. 标记为已刷新
segment.status = FLUSHED;
segment.parentFlushed = true;
// 2. 输出所有chunks
const chunks = segment.chunks;
let chunkIdx = 0;
for (; chunkIdx < chunks.length; chunkIdx++) {
const chunk = chunks[chunkIdx];
if (!writeChunkAndReturn(destination, chunk)) {
// 流已满,暂停刷新
segment.chunks.splice(0, chunkIdx);
return false;
}
}
// 3. 清空chunks
segment.chunks.length = 0;
// 4. 刷新子Segment
const children = segment.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (!flushSegment(request, destination, child)) {
return false;
}
}
return true;
}8.4.3 HTML片段的拼接
Fizz通过pushXXX函数将HTML片段添加到Segment的chunks数组中。
pushTextInstance:推送文本
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
function pushTextInstance(
target: Array<Chunk>,
text: string,
renderState: RenderState,
textEmbedded: boolean,
): boolean {
if (text === '') {
return textEmbedded;
}
if (textEmbedded) {
// 需要转义HTML特殊字符
target.push(escapeTextForBrowser(text));
} else {
// 第一个文本节点,不需要转义
target.push(text);
}
return true;
}pushStartInstance:推送开始标签
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:3000-3100(React 19.3.0,简化版)
function pushStartInstance(
target: Array<Chunk>,
type: string,
props: Object,
renderState: RenderState,
formatContext: FormatContext,
): ReactNodeList {
// 1. 推送开始标签
target.push('<', type);
// 2. 推送属性
for (const propKey in props) {
if (!hasOwnProperty.call(props, propKey)) {
continue;
}
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
// children单独处理
break;
case 'dangerouslySetInnerHTML':
// innerHTML单独处理
break;
case 'style':
pushStyle(target, propValue);
break;
case 'className':
pushAttribute(target, 'class', propValue);
break;
default:
if (isAttributeNameSafe(propKey)) {
pushAttribute(target, propKey, propValue);
}
break;
}
}
// 3. 关闭开始标签
target.push('>');
// 4. 返回children
return props.children;
}pushEndInstance:推送结束标签
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
function pushEndInstance(
target: Array<Chunk>,
type: string,
props: Object,
): void {
// 自闭合标签不需要结束标签
if (isVoidElement(type)) {
return;
}
target.push('</', type, '>');
}示例:Segment的构建过程
jsx
function Component() {
return (
<div className="container">
<h1>Hello</h1>
<p>World</p>
</div>
);
}
// Segment构建过程:
// 1. 创建rootSegment
// chunks: []
// 2. 渲染<div>
// pushStartInstance(chunks, 'div', { className: 'container' })
// chunks: ['<div class="container">']
// 3. 渲染<h1>
// pushStartInstance(chunks, 'h1', {})
// chunks: ['<div class="container">', '<h1>']
// pushTextInstance(chunks, 'Hello')
// chunks: ['<div class="container">', '<h1>', 'Hello']
// pushEndInstance(chunks, 'h1')
// chunks: ['<div class="container">', '<h1>', 'Hello', '</h1>']
// 4. 渲染<p>
// pushStartInstance(chunks, 'p', {})
// chunks: [..., '<p>']
// pushTextInstance(chunks, 'World')
// chunks: [..., '<p>', 'World']
// pushEndInstance(chunks, 'p')
// chunks: [..., '<p>', 'World', '</p>']
// 5. 结束<div>
// pushEndInstance(chunks, 'div')
// chunks: [..., '</div>']
// 6. 刷新Segment
// 输出:<div class="container"><h1>Hello</h1><p>World</p></div>Chunk的类型
javascript
// Chunk可以是字符串或Uint8Array
type Chunk = string | Uint8Array;
// 字符串Chunk:直接输出
const chunk1: Chunk = '<div>';
// Uint8Array Chunk:用于二进制数据
const chunk2: Chunk = new Uint8Array([60, 100, 105, 118, 62]); // '<div>'8.5 Suspense在SSR中的处理
Suspense是流式SSR的核心特性,它允许服务器先发送fallback,等内容就绪后再替换。
8.5.1 Suspense边界识别
当Fizz遇到Suspense组件时,会创建一个SuspenseBoundary对象。
SuspenseBoundary数据结构
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:450-500(React 19.3.0)
type SuspenseBoundary = {
status: SuspenseBoundaryStatus, // 状态
id: number, // Boundary ID
rootSegmentID: number, // 根Segment ID
parentFlushed: boolean, // 父Boundary是否已刷新
pendingTasks: number, // 待处理任务数
completedSegments: Array<Segment>, // 完成的Segment
byteSize: number, // 字节大小
fallbackAbortableTasks: Set<Task>, // fallback的可中止任务
errorDigest: null | string, // 错误摘要
contentState: BoundaryContentState, // 内容状态
fallbackState: BoundaryFallbackState, // fallback状态
};
// Boundary状态
const PENDING = 0; // 待处理
const COMPLETED = 1; // 已完成
const CLIENT_RENDERED = 2; // 客户端渲染createSuspenseBoundary:创建Suspense边界
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:900-950(React 19.3.0,简化版)
function createSuspenseBoundary(
request: Request,
fallbackAbortableTasks: Set<Task>,
): SuspenseBoundary {
return {
status: PENDING,
id: -1, // 延迟分配ID
rootSegmentID: -1,
parentFlushed: false,
pendingTasks: 0,
completedSegments: [],
byteSize: 0,
fallbackAbortableTasks,
errorDigest: null,
contentState: PENDING,
fallbackState: PENDING,
};
}8.5.2 fallback的流式输出
当遇到Suspense时,Fizz会先渲染fallback并立即输出。
renderSuspenseBoundary:渲染Suspense边界
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:4500-4600(React 19.3.0,简化版)
function renderSuspenseBoundary(
request: Request,
task: Task,
props: Object,
): void {
// 1. 创建Suspense边界
const boundary = createSuspenseBoundary(
request,
new Set(),
);
// 2. 分配Boundary ID
const id = (boundary.id = request.nextSegmentId++);
// 3. 推送Suspense容器开始标记
const target = task.blockedSegment.chunks;
pushStartCompletedSuspenseBoundary(target, id);
// 4. 创建内容Segment(延迟渲染)
const contentRootSegment = createPendingSegment(
request,
0,
boundary,
task.formatContext,
false,
false,
);
boundary.rootSegmentID = contentRootSegment.id;
// 5. 创建内容Task
const contentTask = createRenderTask(
request,
null,
props.children, // 实际内容
-1,
boundary,
contentRootSegment,
boundary.fallbackAbortableTasks,
task.keyPath,
task.formatContext,
task.legacyContext,
task.context,
task.treeContext,
);
// 6. 将内容Task加入队列(延迟处理)
request.pingedTasks.push(contentTask);
// 7. 渲染fallback(立即输出)
const fallbackSegment = createPendingSegment(
request,
0,
null,
task.formatContext,
false,
false,
);
fallbackSegment.parentFlushed = true;
task.blockedSegment = fallbackSegment;
try {
// 渲染fallback内容
renderNode(request, task, props.fallback, -1);
fallbackSegment.status = COMPLETED;
// 推送fallback到输出
pushCompletedSegment(
request,
task.blockedSegment.chunks,
fallbackSegment,
);
} catch (error) {
fallbackSegment.status = ERRORED;
boundary.status = CLIENT_RENDERED;
boundary.errorDigest = logRecoverableError(request, error);
}
// 8. 推送Suspense容器结束标记
pushEndCompletedSuspenseBoundary(target);
}pushStartCompletedSuspenseBoundary:推送Suspense开始标记
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
function pushStartCompletedSuspenseBoundary(
target: Array<Chunk>,
id: number,
): void {
// 推送一个带有ID的模板标记
target.push('<!--$?--><template id="B:', id.toString(16), '">');
}
function pushEndCompletedSuspenseBoundary(target: Array<Chunk>): void {
target.push('</template><!--/$-->');
}输出的HTML结构
html
<!--$?-->
<template id="B:0">
<div class="spinner">Loading...</div>
</template>
<!--/$-->这个HTML结构的含义:
<!--$?-->:Suspense边界开始标记<template id="B:0">:fallback内容的容器,ID为0<!--/$-->:Suspense边界结束标记
8.5.3 内容就绪后的替换
当Suspense的内容渲染完成后,Fizz会发送替换脚本。
flushCompletedBoundary:刷新完成的Boundary
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:2400-2500(React 19.3.0,简化版)
function flushCompletedBoundary(
request: Request,
destination: Destination,
boundary: SuspenseBoundary,
): boolean {
const id = boundary.id;
// 1. 推送隐藏的div容器
writeChunk(
destination,
'<div hidden id="S:' + id.toString(16) + '">',
);
// 2. 刷新所有完成的Segment
const completedSegments = boundary.completedSegments;
for (let i = 0; i < completedSegments.length; i++) {
const segment = completedSegments[i];
if (!flushSegment(request, destination, segment)) {
// 流已满,暂停刷新
boundary.completedSegments.splice(0, i);
return false;
}
}
boundary.completedSegments.length = 0;
// 3. 关闭div容器
writeChunk(destination, '</div>');
// 4. 推送替换脚本
writeChunk(
destination,
'<script>$RC("B:' + id.toString(16) + '","S:' + id.toString(16) + '")</script>',
);
return true;
}替换脚本的工作原理
html
<!-- 1. 初始HTML(fallback) -->
<!--$?-->
<template id="B:0">
<div class="spinner">Loading...</div>
</template>
<!--/$-->
<!-- 2. 内容就绪后,服务器发送 -->
<div hidden id="S:0">
<div class="content">
<h1>Loaded Content</h1>
<p>This is the actual content.</p>
</div>
</div>
<script>$RC("B:0","S:0")</script>
<!-- 3. 客户端执行$RC函数 -->
<script>
function $RC(boundaryID, contentID) {
// 找到template和隐藏的div
const template = document.getElementById(boundaryID);
const content = document.getElementById(contentID);
// 用实际内容替换template
const parent = template.parentNode;
const children = content.children;
// 将content的子节点移动到template的位置
while (children.length > 0) {
parent.insertBefore(children[0], template);
}
// 移除template和隐藏的div
parent.removeChild(template);
content.parentNode.removeChild(content);
}
</script>
<!-- 4. 最终HTML -->
<!--$?-->
<div class="content">
<h1>Loaded Content</h1>
<p>This is the actual content.</p>
</div>
<!--/$-->8.5.4 示例:流式SSR的输出过程
让我们通过一个完整的示例来理解Suspense在SSR中的处理。
jsx
// App.js
function App() {
return (
<html>
<head>
<title>Streaming SSR</title>
</head>
<body>
<Header />
<Suspense fallback={<Spinner />}>
<SlowContent />
</Suspense>
<Footer />
</body>
</html>
);
}
function Header() {
return <header>Header</header>;
}
function Footer() {
return <footer>Footer</footer>;
}
function Spinner() {
return <div className="spinner">Loading...</div>;
}
async function SlowContent() {
// 模拟慢速数据获取
const data = await fetchData(); // 3秒
return <div className="content">{data}</div>;
}输出时间线
时间 0ms:客户端发起请求
时间 10ms:服务器开始渲染
- 渲染<Header />
- 遇到<Suspense>,创建Boundary
- 渲染<Spinner />(fallback)
- 创建延迟Task渲染<SlowContent />
- 渲染<Footer />
- Shell完成,触发onShellReady
时间 50ms:发送Shell HTMLhtml
<!DOCTYPE html>
<html>
<head>
<title>Streaming SSR</title>
</head>
<body>
<header>Header</header>
<!--$?-->
<template id="B:0">
<div class="spinner">Loading...</div>
</template>
<!--/$-->
<footer>Footer</footer>
</body>
</html>时间 3000ms:<SlowContent />的Promise resolve
- pingTask被调用
- 延迟Task加入pingedTasks
- performWork执行Task
- 渲染<SlowContent />
- Boundary完成
- 刷新Boundary的HTML
时间 3010ms:发送内容替换HTMLhtml
<div hidden id="S:0">
<div class="content">Loaded Data</div>
</div>
<script>$RC("B:0","S:0")</script>时间 3010ms:客户端执行替换脚本
- 找到template#B:0和div#S:0
- 用实际内容替换template
- 移除template和隐藏的div
最终HTML:html
<!DOCTYPE html>
<html>
<head>
<title>Streaming SSR</title>
</head>
<body>
<header>Header</header>
<!--$?-->
<div class="content">Loaded Data</div>
<!--/$-->
<footer>Footer</footer>
</body>
</html>用户体验对比
传统SSR:
0-3000ms:白屏等待
3000ms:看到完整页面
流式SSR:
0-50ms:白屏等待
50ms:看到Header、Spinner、Footer
3010ms:Spinner替换为实际内容嵌套Suspense
jsx
function App() {
return (
<div>
<Suspense fallback={<Spinner1 />}>
<SlowContent1 />
<Suspense fallback={<Spinner2 />}>
<SlowContent2 />
</Suspense>
</Suspense>
</div>
);
}
// 输出顺序:
// 1. Shell:<Spinner1 />
// 2. SlowContent1完成:替换外层Suspense,显示<SlowContent1 />和<Spinner2 />
// 3. SlowContent2完成:替换内层Suspense,显示<SlowContent2 />8.6 错误处理
Fizz提供了完善的错误处理机制,支持错误边界和客户端降级。
8.6.1 错误边界
当Suspense边界内的组件抛出错误时,Fizz会捕获错误并标记为客户端渲染。
abortTaskSoft:软中止任务
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1600-1700(React 19.3.0,简化版)
function abortTaskSoft(task: Task, request: Request): void {
const boundary = task.blockedBoundary;
const segment = task.blockedSegment;
if (segment.status !== PENDING) {
// Segment已经完成,不需要中止
return;
}
// 标记Segment为已中止
segment.status = ABORTED;
// 完成任务
finishedTask(request, boundary, segment);
}erroredTask:任务出错
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1500-1600(React 19.3.0,简化版)
function erroredTask(
request: Request,
boundary: Root | SuspenseBoundary,
segment: Segment,
error: mixed,
): void {
// 1. 标记Segment为出错
segment.status = ERRORED;
if (boundary === null) {
// 根任务出错,致命错误
fatalError(request, error);
return;
}
// 2. Suspense边界内的错误
if (boundary.status === PENDING) {
// 标记Boundary为客户端渲染
boundary.status = CLIENT_RENDERED;
// 记录错误摘要
const errorDigest = logRecoverableError(request, error);
boundary.errorDigest = errorDigest;
// 中止所有相关任务
const abortableTasks = boundary.fallbackAbortableTasks;
abortableTasks.forEach(task => abortTaskSoft(task, request));
abortableTasks.clear();
// 将Boundary加入客户端渲染队列
request.clientRenderedBoundaries.push(boundary);
}
// 3. 完成任务
request.allPendingTasks--;
if (request.allPendingTasks === 0) {
const onAllReady = request.onAllReady;
onAllReady();
}
}logRecoverableError:记录可恢复错误
javascript
// 文件:packages/react-server/src/ReactFizzServer.js
function logRecoverableError(request: Request, error: mixed): string {
// 1. 调用用户提供的onError回调
const errorDigest = request.onError(error);
// 2. 如果没有返回digest,生成一个
if (errorDigest != null && typeof errorDigest !== 'string') {
throw new Error(
'onError returned something with a type other than "string". ' +
'onError should return a string and may return null or undefined but ' +
'must not return anything else.'
);
}
return errorDigest || '';
}8.6.2 错误恢复策略
Fizz支持三种错误恢复策略。
1. 客户端渲染(Client Rendering)
当Suspense边界内的组件出错时,Fizz会标记该边界为客户端渲染,让客户端重新渲染该部分。
javascript
// flushClientRenderedBoundary:刷新客户端渲染的边界
function flushClientRenderedBoundary(
request: Request,
destination: Destination,
boundary: SuspenseBoundary,
): boolean {
// 推送客户端渲染指令
return writeChunk(
destination,
'<script>$RX("B:' +
boundary.id.toString(16) +
'","' +
escapeJSStringsForInstructionScripts(boundary.errorDigest || '') +
'")</script>',
);
}客户端渲染脚本
html
<script>
function $RX(boundaryID, digest) {
// 找到Suspense边界
const template = document.getElementById(boundaryID);
// 标记为需要客户端渲染
template.dataset.dgst = digest;
// 触发React客户端重新渲染这个边界
// React会在Hydration时检测到这个标记,并重新渲染
}
</script>2. 错误边界(Error Boundary)
如果Suspense外层有ErrorBoundary,错误会被捕获并显示错误UI。
jsx
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<Spinner />}>
<ErrorProneComponent />
</Suspense>
</ErrorBoundary>
);
}3. 致命错误(Fatal Error)
如果根任务出错(Shell渲染失败),Fizz会调用onShellError回调,让开发者决定如何处理。
javascript
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
res.statusCode = 200;
pipe(res);
},
onShellError(error) {
// Shell渲染失败,返回错误页面
res.statusCode = 500;
res.send(`
<!DOCTYPE html>
<html>
<body>
<h1>Server Error</h1>
<p>Sorry, something went wrong.</p>
</body>
</html>
`);
},
onError(error) {
// 记录错误
console.error('Render error:', error);
return 'error-digest-' + Date.now();
},
});8.6.3 客户端降级
当服务端渲染失败时,客户端会接管渲染工作。
降级流程
示例:错误处理流程
jsx
function App() {
return (
<div>
<Header />
<Suspense fallback={<Spinner />}>
<ErrorProneComponent />
</Suspense>
<Footer />
</div>
);
}
function ErrorProneComponent() {
// 模拟错误
throw new Error('Component error');
return <div>Content</div>;
}
// 服务端渲染流程:
// 1. 渲染<Header />:成功
// 2. 遇到<Suspense>:创建Boundary
// 3. 渲染<Spinner />:成功(fallback)
// 4. 创建Task渲染<ErrorProneComponent />
// 5. Shell完成,发送HTML:
// <Header />
// <!--$?--><template id="B:0"><Spinner /></template><!--/$-->
// <Footer />
// 6. 执行延迟Task
// 7. <ErrorProneComponent />抛出错误
// 8. erroredTask被调用
// 9. Boundary标记为CLIENT_RENDERED
// 10. 发送客户端渲染脚本:
// <script>$RX("B:0","error-digest-123")</script>
// 客户端Hydration流程:
// 1. React开始Hydration
// 2. 检测到Boundary#B:0有data-dgst属性
// 3. 跳过服务端HTML
// 4. 客户端重新渲染<ErrorProneComponent />
// 5. 如果仍然出错,触发ErrorBoundary或显示错误错误处理最佳实践
- 使用ErrorBoundary:在Suspense外层包裹ErrorBoundary
- 提供onError回调:记录错误日志,返回错误摘要
- 优雅降级:确保客户端可以正常渲染
- 监控错误率:跟踪客户端渲染的频率
javascript
// 完整的错误处理配置
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
onShellError(error) {
res.statusCode = 500;
res.send(renderErrorPage(error));
},
onError(error) {
// 记录错误到监控系统
logger.error('SSR Error:', {
message: error.message,
stack: error.stack,
url: req.url,
});
// 返回错误摘要(不要暴露敏感信息)
return hashError(error);
},
});8.7 综合案例:实现一个流式SSR服务器
让我们通过一个完整的案例来实践流式SSR的所有知识点。
8.7.1 项目结构
streaming-ssr-demo/
├── package.json
├── server.js # 服务器入口
├── src/
│ ├── App.jsx # 根组件
│ ├── components/
│ │ ├── Header.jsx
│ │ ├── Footer.jsx
│ │ ├── Spinner.jsx
│ │ ├── UserProfile.jsx
│ │ ├── Comments.jsx
│ │ └── Recommendations.jsx
│ └── client.jsx # 客户端入口
└── public/
└── client.js # 客户端bundle8.7.2 服务器实现
javascript
// server.js
import express from 'express';
import { renderToPipeableStream } from 'react-dom/server';
import { App } from './src/App.jsx';
const app = express();
// 静态文件服务
app.use(express.static('public'));
// SSR路由
app.get('/', (req, res) => {
const { pipe, abort } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/client.js'],
onShellReady() {
// Shell就绪,立即发送
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
onShellError(error) {
// Shell错误,返回错误页面
res.statusCode = 500;
res.send(`
<!DOCTYPE html>
<html>
<body>
<h1>Server Error</h1>
<p>Sorry, something went wrong.</p>
</body>
</html>
`);
},
onAllReady() {
// 所有内容就绪
console.log('All content ready');
},
onError(error) {
// 错误处理
console.error('Render error:', error);
return `error-${Date.now()}`;
},
});
// 30秒超时
req.setTimeout(30000, () => {
console.log('Request timeout, aborting render');
abort();
});
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});8.7.3 应用组件
jsx
// src/App.jsx
import { Suspense } from 'react';
import { Header } from './components/Header';
import { Footer } from './components/Footer';
import { Spinner } from './components/Spinner';
import { UserProfile } from './components/UserProfile';
import { Comments } from './components/Comments';
import { Recommendations } from './components/Recommendations';
export function App() {
return (
<html>
<head>
<title>Streaming SSR Demo</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<Header />
<main>
{/* 用户资料 - 快速加载 */}
<Suspense fallback={<Spinner text="Loading profile..." />}>
<UserProfile userId="123" />
</Suspense>
{/* 评论列表 - 中速加载 */}
<Suspense fallback={<Spinner text="Loading comments..." />}>
<Comments postId="456" />
</Suspense>
{/* 推荐内容 - 慢速加载 */}
<Suspense fallback={<Spinner text="Loading recommendations..." />}>
<Recommendations userId="123" />
</Suspense>
</main>
<Footer />
</body>
</html>
);
}jsx
// src/components/Header.jsx
export function Header() {
return (
<header className="header">
<h1>Streaming SSR Demo</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
);
}jsx
// src/components/Footer.jsx
export function Footer() {
return (
<footer className="footer">
<p>© 2024 Streaming SSR Demo</p>
</footer>
);
}jsx
// src/components/Spinner.jsx
export function Spinner({ text = 'Loading...' }) {
return (
<div className="spinner">
<div className="spinner-icon"></div>
<p>{text}</p>
</div>
);
}8.7.4 异步组件
jsx
// src/components/UserProfile.jsx
async function fetchUserProfile(userId) {
// 模拟API调用 - 1秒
await new Promise(resolve => setTimeout(resolve, 1000));
return {
id: userId,
name: 'John Doe',
avatar: '/avatar.jpg',
bio: 'Software Engineer',
};
}
export async function UserProfile({ userId }) {
const user = await fetchUserProfile(userId);
return (
<div className="user-profile">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}jsx
// src/components/Comments.jsx
async function fetchComments(postId) {
// 模拟API调用 - 2秒
await new Promise(resolve => setTimeout(resolve, 2000));
return [
{ id: 1, author: 'Alice', text: 'Great post!' },
{ id: 2, author: 'Bob', text: 'Thanks for sharing.' },
{ id: 3, author: 'Charlie', text: 'Very helpful!' },
];
}
export async function Comments({ postId }) {
const comments = await fetchComments(postId);
return (
<div className="comments">
<h3>Comments ({comments.length})</h3>
<ul>
{comments.map(comment => (
<li key={comment.id}>
<strong>{comment.author}:</strong> {comment.text}
</li>
))}
</ul>
</div>
);
}jsx
// src/components/Recommendations.jsx
async function fetchRecommendations(userId) {
// 模拟API调用 - 3秒
await new Promise(resolve => setTimeout(resolve, 3000));
return [
{ id: 1, title: 'React 19 Features', url: '/post/1' },
{ id: 2, title: 'Streaming SSR Guide', url: '/post/2' },
{ id: 3, title: 'Server Components', url: '/post/3' },
];
}
export async function Recommendations({ userId }) {
const recommendations = await fetchRecommendations(userId);
return (
<div className="recommendations">
<h3>Recommended for You</h3>
<ul>
{recommendations.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</div>
);
}8.7.5 客户端Hydration
jsx
// src/client.jsx
import { hydrateRoot } from 'react-dom/client';
import { App } from './App';
// Hydration
hydrateRoot(document, <App />);8.7.6 运行效果
时间线
时间 0ms:客户端发起请求
时间 50ms:Shell就绪
- 发送HTML:Header + 3个Spinner + Footer
- 用户看到页面结构
时间 1000ms:UserProfile完成
- 发送UserProfile的HTML
- 客户端执行替换脚本
- 第一个Spinner替换为用户资料
时间 2000ms:Comments完成
- 发送Comments的HTML
- 客户端执行替换脚本
- 第二个Spinner替换为评论列表
时间 3000ms:Recommendations完成
- 发送Recommendations的HTML
- 客户端执行替换脚本
- 第三个Spinner替换为推荐内容
- 所有内容加载完成网络瀑布图
0ms 50ms 1000ms 2000ms 3000ms
|-------|-------|-------|-------|
| | | | |
| Shell | | | |
|-------| | | |
| | | |
| User | | |
|-------| | |
| | |
| Comm | |
|-------| |
| |
| Recom |
|-------|HTML输出顺序
html
<!-- 1. Shell(50ms) -->
<!DOCTYPE html>
<html>
<head>
<title>Streaming SSR Demo</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<header class="header">
<h1>Streaming SSR Demo</h1>
<nav><a href="/">Home</a><a href="/about">About</a></nav>
</header>
<main>
<!--$?--><template id="B:0">
<div class="spinner">
<div class="spinner-icon"></div>
<p>Loading profile...</p>
</div>
</template><!--/$-->
<!--$?--><template id="B:1">
<div class="spinner">
<div class="spinner-icon"></div>
<p>Loading comments...</p>
</div>
</template><!--/$-->
<!--$?--><template id="B:2">
<div class="spinner">
<div class="spinner-icon"></div>
<p>Loading recommendations...</p>
</div>
</template><!--/$-->
</main>
<footer class="footer">
<p>© 2024 Streaming SSR Demo</p>
</footer>
<script src="/client.js" async></script>
</body>
</html>
<!-- 2. UserProfile(1000ms) -->
<div hidden id="S:0">
<div class="user-profile">
<img src="/avatar.jpg" alt="John Doe" />
<h2>John Doe</h2>
<p>Software Engineer</p>
</div>
</div>
<script>$RC("B:0","S:0")</script>
<!-- 3. Comments(2000ms) -->
<div hidden id="S:1">
<div class="comments">
<h3>Comments (3)</h3>
<ul>
<li><strong>Alice:</strong> Great post!</li>
<li><strong>Bob:</strong> Thanks for sharing.</li>
<li><strong>Charlie:</strong> Very helpful!</li>
</ul>
</div>
</div>
<script>$RC("B:1","S:1")</script>
<!-- 4. Recommendations(3000ms) -->
<div hidden id="S:2">
<div class="recommendations">
<h3>Recommended for You</h3>
<ul>
<li><a href="/post/1">React 19 Features</a></li>
<li><a href="/post/2">Streaming SSR Guide</a></li>
<li><a href="/post/3">Server Components</a></li>
</ul>
</div>
</div>
<script>$RC("B:2","S:2")</script>8.7.7 性能优化
1. 并行数据获取
jsx
// ✗ 不好:串行获取
async function SlowComponent() {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return <div>...</div>;
}
// ✓ 好:并行获取
async function FastComponent() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
return <div>...</div>;
}2. 合理划分Suspense边界
jsx
// ✗ 不好:一个大的Suspense
<Suspense fallback={<BigSpinner />}>
<FastComponent />
<SlowComponent />
</Suspense>
// 问题:FastComponent也要等SlowComponent
// ✓ 好:多个小的Suspense
<Suspense fallback={<Spinner />}>
<FastComponent />
</Suspense>
<Suspense fallback={<Spinner />}>
<SlowComponent />
</Suspense>
// 优势:FastComponent可以先显示3. 预加载关键数据
jsx
// 在渲染前预加载关键数据
const userPromise = fetchUser();
const postsPromise = fetchPosts();
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile promise={userPromise} />
<PostList promise={postsPromise} />
</Suspense>
);
}4. 使用缓存
javascript
// 简单的内存缓存
const cache = new Map();
async function fetchWithCache(key, fetcher) {
if (cache.has(key)) {
return cache.get(key);
}
const data = await fetcher();
cache.set(key, data);
// 5分钟后过期
setTimeout(() => cache.delete(key), 5 * 60 * 1000);
return data;
}
async function UserProfile({ userId }) {
const user = await fetchWithCache(
`user:${userId}`,
() => fetchUser(userId)
);
return <div>{user.name}</div>;
}8.7.8 监控与调试
1. 性能监控
javascript
// server.js
app.get('/', (req, res) => {
const startTime = Date.now();
let shellTime = 0;
let allReadyTime = 0;
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
shellTime = Date.now() - startTime;
console.log(`Shell ready in ${shellTime}ms`);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.setHeader('X-Shell-Time', shellTime);
pipe(res);
},
onAllReady() {
allReadyTime = Date.now() - startTime;
console.log(`All ready in ${allReadyTime}ms`);
res.setHeader('X-All-Ready-Time', allReadyTime);
},
onError(error) {
console.error('Render error:', {
message: error.message,
stack: error.stack,
url: req.url,
userAgent: req.headers['user-agent'],
});
// 发送到监控系统
sendToMonitoring({
type: 'ssr-error',
error: error.message,
url: req.url,
});
return `error-${Date.now()}`;
},
});
});2. 调试工具
javascript
// 开发环境下的详细日志
if (process.env.NODE_ENV === 'development') {
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
console.log('=== Shell Ready ===');
console.log('Pending tasks:', request.allPendingTasks);
pipe(res);
},
onAllReady() {
console.log('=== All Ready ===');
console.log('Total time:', Date.now() - startTime, 'ms');
},
onError(error) {
console.error('=== Error ===');
console.error(error);
return 'dev-error';
},
});
}3. 性能指标
javascript
// 收集性能指标
const metrics = {
ttfb: 0, // Time to First Byte
shellTime: 0, // Shell渲染时间
allReadyTime: 0, // 全部就绪时间
errorCount: 0, // 错误数量
clientRenderCount: 0, // 客户端渲染次数
};
// 定期上报
setInterval(() => {
sendMetrics(metrics);
// 重置计数器
metrics.errorCount = 0;
metrics.clientRenderCount = 0;
}, 60000); // 每分钟上报一次本章小结
本章深入讲解了React流式SSR(Fizz)的实现原理,让我们回顾一下关键要点:
Fizz架构
- 流式输出:边渲染边发送,不需要等待整个页面完成
- Task任务系统:通过Task组织渲染工作,支持异步和优先级
- Segment分段渲染:HTML片段的基本单位,支持独立输出
- Suspense支持:先发送fallback,内容就绪后替换
核心概念
- Request对象:渲染请求的核心,保存所有状态
- Task对象:渲染任务,负责渲染一部分React节点
- Segment对象:HTML片段,包含chunks数组
- SuspenseBoundary对象:Suspense边界,管理fallback和内容
渲染流程
- 创建Request:初始化渲染状态
- 创建根Task:开始渲染
- 遇到Suspense:创建Boundary,渲染fallback
- Shell完成:触发onShellReady,发送HTML
- 延迟内容完成:发送替换脚本
- 所有完成:触发onAllReady
错误处理
- 客户端渲染:Suspense边界内的错误标记为客户端渲染
- 错误边界:使用ErrorBoundary捕获错误
- 致命错误:Shell渲染失败,调用onShellError
最佳实践
- 使用onShellReady:立即发送Shell,提升首字节时间
- 合理划分Suspense:避免一个大的Suspense阻塞所有内容
- 并行数据获取:使用Promise.all并行获取数据
- 错误监控:记录错误日志,跟踪客户端渲染频率
思考题
- 为什么流式SSR比传统SSR更快?
- Task和Segment的关系是什么?
- Suspense的fallback是如何被替换的?
- 什么情况下会触发客户端渲染?
在下一章中,我们将学习React Server Components(RSC)和Flight协议,这是React 19全栈架构的核心特性。