流式渲染
什么是流式渲染
renderToString 会等待整个组件树渲染完毕后,一次性返回完整的 HTML 字符串。如果组件中有异步操作(如数据请求),服务器必须等所有异步操作完成后才能发送响应。
流式渲染则不同——它逐块输出 HTML 内容,服务器不需要等待全部渲染完成就能开始发送数据。浏览器收到第一块 HTML 后就可以开始解析和显示,用户更快地看到页面内容。
简单对比:
- renderToString:渲染完成 → 一次性发送 → 用户等待
- 流式渲染:边渲染边发送 → 用户逐步看到内容
流式渲染的优势
更快的 TTFB
TTFB(Time To First Byte)是浏览器收到服务器第一个字节的时间。流式渲染可以在组件树开始渲染时就发送数据,大幅缩短 TTFB。
更好的用户体验
用户不需要等到所有数据加载完成才能看到页面。页面的"骨架"和同步内容会立即显示,异步内容在数据就绪后追加。
renderToStream
renderToStream 是流式渲染的基础函数,通过回调函数逐块输出 HTML 内容。
import { createSSRApp } from '@vitarx/runtime-ssr'
import { renderToStream } from '@vitarx/runtime-ssr'
const app = createSSRApp(App)
const context = {}
await renderToStream(app, context, {
push(content) {
// 每渲染出一块 HTML 就调用此函数
res.write(content)
},
close() {
// 渲染完成,关闭流
res.end()
},
error(err) {
// 渲染出错
console.error(err)
}
})函数签名
declare function renderToStream(
root: SSRApp | View,
context: SSRContext,
options: StreamingSink
): Promise<void>| 参数 | 类型 | 说明 |
|---|---|---|
| root | SSRApp | View | SSR 应用实例或虚拟节点 |
| context | SSRContext | SSR 上下文对象(可选) |
| options | StreamingSink | 流式输出回调 |
StreamingSink 接口
interface StreamingSink {
/** 接收一块 HTML 内容 */
push(content: string): void
/** 渲染完成,关闭流 */
close(): void
/** 渲染出错 */
error(error: unknown): void
}renderToReadableStream
renderToReadableStream 返回一个 Web 标准的 ReadableStream<string>,适用于支持 Web Streams API 的环境(如 Cloudflare Workers、Deno 等)。
import { createSSRApp } from '@vitarx/runtime-ssr'
import { renderToReadableStream } from '@vitarx/runtime-ssr'
const app = createSSRApp(App)
const context = {}
const stream = renderToReadableStream(app, context)
// 直接作为 Response 的 body
return new Response(stream, {
headers: { 'Content-Type': 'text/html' }
})函数签名
declare function renderToReadableStream(
root: SSRApp | View,
context?: SSRContext
): ReadableStream<string>| 参数 | 类型 | 说明 |
|---|---|---|
| root | SSRApp | View | SSR 应用实例或虚拟节点 |
| context | SSRContext | SSR 上下文对象(可选) |
返回值:ReadableStream<string> — Web 标准的 ReadableStream
renderToNodeStream
renderToNodeStream 返回一个 Node.js 的 Readable 流,适用于 Node.js 环境。
import { createSSRApp } from '@vitarx/runtime-ssr'
import { renderToNodeStream } from '@vitarx/runtime-ssr'
const app = createSSRApp(App)
const context = {}
const stream = await renderToNodeStream(app, context)
// 直接管道到 HTTP 响应
stream.pipe(res)函数签名
declare function renderToNodeStream(
root: SSRApp | View,
context?: SSRContext
): Promise<NodeJS.ReadableStream>| 参数 | 类型 | 说明 |
|---|---|---|
| root | SSRApp | View | SSR 应用实例或虚拟节点 |
| context | SSRContext | SSR 上下文对象(可选) |
返回值:Promise<NodeJS.ReadableStream> — Node.js Readable 流
WARNING
renderToNodeStream 只能在 Node.js 环境中使用,在浏览器中调用会抛出错误。
pipeToWritable
pipeToWritable 将渲染结果直接写入一个 Node.js Writable 流(如 http.ServerResponse),是最简洁的 Node.js 流式渲染方式。
import { createSSRApp } from '@vitarx/runtime-ssr'
import { pipeToWritable } from '@vitarx/runtime-ssr'
const app = createSSRApp(App)
const context = {}
// 直接管道到 HTTP 响应
await pipeToWritable(app, res, context)函数签名
declare function pipeToWritable(
root: SSRApp | View,
writable: NodeJS.WritableStream,
context?: SSRContext
): Promise<void>| 参数 | 类型 | 说明 |
|---|---|---|
| root | SSRApp | View | SSR 应用实例或虚拟节点 |
| writable | NodeJS.WritableStream | 目标可写流 |
| context | SSRContext | SSR 上下文对象(可选) |
流式渲染 API 对比
| API | 返回值 | 适用环境 | 说明 |
|---|---|---|---|
renderToStream | Promise<void> | 通用 | 底层 API,通过回调输出 |
renderToReadableStream | ReadableStream<string> | Web 标准环境 | Cloudflare Workers、Deno 等 |
renderToNodeStream | Promise<NodeJS.ReadableStream> | Node.js | 返回 Node.js Readable 流 |
pipeToWritable | Promise<void> | Node.js | 直接写入 Writable 流,最简洁 |
流式渲染与 Suspense 配合
流式渲染与 Suspense 组件天然配合。当遇到异步组件时,流式渲染会先输出 Suspense 的 fallback 内容,等异步组件加载完成后再输出实际内容。
import { Suspense } from 'vitarx'
function App() {
return (
<div>
<h1>流式渲染示例</h1>
{/* 同步内容会立即输出 */}
<p>这段内容会立即显示</p>
{/* 异步内容会先显示 fallback,数据就绪后输出实际内容 */}
<Suspense fallback={<p>加载中...</p>}>
<AsyncArticle />
</Suspense>
</div>
)
}流式渲染的输出过程:
- 立即输出
<div><h1>流式渲染示例</h1><p>这段内容会立即显示</p> - 输出
<p>加载中...</p>(Suspense 的 fallback) - 等待
AsyncArticle数据加载完成 - 输出
AsyncArticle的实际内容 - 关闭流
完整示例
下面是一个使用 pipeToWritable 的 Node.js Express 服务端流式渲染示例。
根组件 App.tsx
import { ref, onMounted } from 'vitarx'
import { useSSRContext } from '@vitarx/runtime-ssr'
interface AppContext {
articles: string[]
}
export default function App() {
const ctx = useSSRContext<AppContext>()
const articles = ref<string[]>(ctx?.articles ?? [])
return (
<div>
<h1>流式渲染示例</h1>
<ul>
{articles.value.map((article) => (
<li key={article}>{article}</li>
))}
</ul>
</div>
)
}服务端入口 server.ts
import express from 'express'
import { createSSRApp } from '@vitarx/runtime-ssr'
import { pipeToWritable } from '@vitarx/runtime-ssr'
import App from './App'
const server = express()
server.get('/', async (req, res) => {
// 预取数据
const articles = await fetchArticles()
const context = { articles }
// 写入 HTML 头部
res.write(`
<!DOCTYPE html>
<html>
<head><meta charset="utf-8" /><title>流式渲染示例</title></head>
<body>
<div id="app">
`)
// 流式渲染应用内容
const app = createSSRApp(App)
await pipeToWritable(app, res, context)
// 写入 HTML 尾部
res.write(`
</div>
<script>window.__INITIAL_STATE__ = #123;JSON.stringify(context)}</script>
<script src="/client.js"></script>
</body>
</html>
`)
res.end()
})
server.listen(3000, () => {
console.log('服务运行在 http://localhost:3000')
})使用 renderToReadableStream 的边缘环境示例
// Cloudflare Workers 等边缘环境
import { createSSRApp } from '@vitarx/runtime-ssr'
import { renderToReadableStream } from '@vitarx/runtime-ssr'
import App from './App'
export default {
async fetch(request) {
const context = {}
const app = createSSRApp(App)
const stream = renderToReadableStream(app, context)
return new Response(stream, {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
})
}
}