流式渲染

什么是流式渲染

renderToString 会等待整个组件树渲染完毕后,一次性返回完整的 HTML 字符串。如果组件中有异步操作(如数据请求),服务器必须等所有异步操作完成后才能发送响应。

流式渲染则不同——它逐块输出 HTML 内容,服务器不需要等待全部渲染完成就能开始发送数据。浏览器收到第一块 HTML 后就可以开始解析和显示,用户更快地看到页面内容。

简单对比:

  • renderToString:渲染完成 → 一次性发送 → 用户等待
  • 流式渲染:边渲染边发送 → 用户逐步看到内容

流式渲染的优势

更快的 TTFB

TTFB(Time To First Byte)是浏览器收到服务器第一个字节的时间。流式渲染可以在组件树开始渲染时就发送数据,大幅缩短 TTFB。

更好的用户体验

用户不需要等到所有数据加载完成才能看到页面。页面的"骨架"和同步内容会立即显示,异步内容在数据就绪后追加。

renderToStream

renderToStream 是流式渲染的基础函数,通过回调函数逐块输出 HTML 内容。

tsx
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)
  }
})

函数签名

typescript
declare function renderToStream(
  root: SSRApp | View,
  context: SSRContext,
  options: StreamingSink
): Promise<void>
参数类型说明
rootSSRApp | ViewSSR 应用实例或虚拟节点
contextSSRContextSSR 上下文对象(可选)
optionsStreamingSink流式输出回调

StreamingSink 接口

typescript
interface StreamingSink {
  /** 接收一块 HTML 内容 */
  push(content: string): void
  /** 渲染完成,关闭流 */
  close(): void
  /** 渲染出错 */
  error(error: unknown): void
}

renderToReadableStream

renderToReadableStream 返回一个 Web 标准的 ReadableStream<string>,适用于支持 Web Streams API 的环境(如 Cloudflare Workers、Deno 等)。

tsx
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' }
})

函数签名

typescript
declare function renderToReadableStream(
  root: SSRApp | View,
  context?: SSRContext
): ReadableStream<string>
参数类型说明
rootSSRApp | ViewSSR 应用实例或虚拟节点
contextSSRContextSSR 上下文对象(可选)

返回值ReadableStream<string> — Web 标准的 ReadableStream

renderToNodeStream

renderToNodeStream 返回一个 Node.js 的 Readable 流,适用于 Node.js 环境。

tsx
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)

函数签名

typescript
declare function renderToNodeStream(
  root: SSRApp | View,
  context?: SSRContext
): Promise<NodeJS.ReadableStream>
参数类型说明
rootSSRApp | ViewSSR 应用实例或虚拟节点
contextSSRContextSSR 上下文对象(可选)

返回值Promise<NodeJS.ReadableStream> — Node.js Readable 流

WARNING

renderToNodeStream 只能在 Node.js 环境中使用,在浏览器中调用会抛出错误。

pipeToWritable

pipeToWritable 将渲染结果直接写入一个 Node.js Writable 流(如 http.ServerResponse),是最简洁的 Node.js 流式渲染方式。

tsx
import { createSSRApp } from '@vitarx/runtime-ssr'
import { pipeToWritable } from '@vitarx/runtime-ssr'

const app = createSSRApp(App)
const context = {}

// 直接管道到 HTTP 响应
await pipeToWritable(app, res, context)

函数签名

typescript
declare function pipeToWritable(
  root: SSRApp | View,
  writable: NodeJS.WritableStream,
  context?: SSRContext
): Promise<void>
参数类型说明
rootSSRApp | ViewSSR 应用实例或虚拟节点
writableNodeJS.WritableStream目标可写流
contextSSRContextSSR 上下文对象(可选)

流式渲染 API 对比

API返回值适用环境说明
renderToStreamPromise<void>通用底层 API,通过回调输出
renderToReadableStreamReadableStream<string>Web 标准环境Cloudflare Workers、Deno 等
renderToNodeStreamPromise<NodeJS.ReadableStream>Node.js返回 Node.js Readable 流
pipeToWritablePromise<void>Node.js直接写入 Writable 流,最简洁

流式渲染与 Suspense 配合

流式渲染与 Suspense 组件天然配合。当遇到异步组件时,流式渲染会先输出 Suspense 的 fallback 内容,等异步组件加载完成后再输出实际内容。

tsx
import { Suspense } from 'vitarx'

function App() {
  return (
    <div>
      <h1>流式渲染示例</h1>
      {/* 同步内容会立即输出 */}
      <p>这段内容会立即显示</p>

      {/* 异步内容会先显示 fallback,数据就绪后输出实际内容 */}
      <Suspense fallback={<p>加载中...</p>}>
        <AsyncArticle />
      </Suspense>
    </div>
  )
}

流式渲染的输出过程:

  1. 立即输出 <div><h1>流式渲染示例</h1><p>这段内容会立即显示</p>
  2. 输出 <p>加载中...</p>(Suspense 的 fallback)
  3. 等待 AsyncArticle 数据加载完成
  4. 输出 AsyncArticle 的实际内容
  5. 关闭流

完整示例

下面是一个使用 pipeToWritable 的 Node.js Express 服务端流式渲染示例。

根组件 App.tsx

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

tsx
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 的边缘环境示例

tsx
// 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' }
    })
  }
}

下一步

  • SSR 基础 — 回顾 SSR 的基本概念和 renderToString
  • 客户端水合 — 了解流式渲染输出后的客户端水合流程
  • SSR 上下文 — 了解流式渲染中的上下文状态管理