Appearance
流式渲染:Streaming SSR
renderToString 需要等待整个组件树渲染完成才能返回。对于大型应用,这可能导致很长的等待时间。
流式渲染边渲染边发送,让用户更快看到内容。 本章将分析其实现原理。
传统渲染 vs 流式渲染
传统渲染:
服务器接收请求
│
▼
完整渲染整个组件树(可能需要数秒)
│
▼
发送完整 HTML
│
▼
用户看到内容流式渲染:
服务器接收请求
│
▼
立即开始发送 HTML
│
├─▶ 发送 <head>
│ 用户开始加载 CSS/JS
│
├─▶ 发送第一个组件
│ 用户开始看到内容
│
├─▶ 发送更多组件
│ 用户继续看到更多内容
│
▼
渲染完成流式 SSR API
javascript
import {
renderToNodeStream, // Node.js Readable Stream
renderToWebStream, // Web ReadableStream
pipeToNodeWritable, // 管道到 Node.js Writable
pipeToWebWritable // 管道到 Web WritableStream
} from 'vue/server-renderer'renderToNodeStream
javascript
import { Readable } from 'stream'
function renderToNodeStream(input, context = {}) {
const stream = new Readable({
read() {}
})
const renderTask = async () => {
try {
const renderer = createStreamRenderer(
chunk => stream.push(chunk),
error => stream.destroy(error)
)
await renderer.render(input, context)
stream.push(null) // 标记结束
} catch (error) {
stream.destroy(error)
}
}
renderTask()
return stream
}流式渲染器
javascript
function createStreamRenderer(push, onError) {
const buffer = []
const flush = () => {
if (buffer.length > 0) {
push(buffer.join(''))
buffer.length = 0
}
}
return {
write(content) {
buffer.push(content)
// 达到阈值时刷新
if (buffer.join('').length > 1024) {
flush()
}
},
async render(input, context) {
const vnode = isApp(input)
? createVNode(input._component, input._props)
: input
await renderVNodeToStream(vnode, context, this)
flush()
}
}
}在 Express 中使用
javascript
import express from 'express'
import { renderToNodeStream } from 'vue/server-renderer'
const app = express()
app.get('*', async (req, res) => {
const vueApp = createSSRApp(App)
res.setHeader('Content-Type', 'text/html')
// 发送 HTML 开头
res.write('<!DOCTYPE html><html><head>...</head><body><div id="app">')
// 流式渲染组件
const stream = renderToNodeStream(vueApp)
stream.pipe(res, { end: false })
stream.on('end', () => {
res.write('</div><script src="/client.js"></script></body></html>')
res.end()
})
})pipeToNodeWritable
更简洁的 API:
javascript
import { pipeToNodeWritable } from 'vue/server-renderer'
app.get('*', async (req, res) => {
const vueApp = createSSRApp(App)
res.setHeader('Content-Type', 'text/html')
res.write('<!DOCTYPE html><html><head>...</head><body><div id="app">')
await pipeToNodeWritable(vueApp, {}, res)
res.write('</div><script src="/client.js"></script></body></html>')
res.end()
})Web Streams API
现代运行时(Deno、Cloudflare Workers)使用 Web Streams:
javascript
import { renderToWebStream } from 'vue/server-renderer'
export default {
async fetch(request) {
const vueApp = createSSRApp(App)
const stream = renderToWebStream(vueApp)
// 包装完整 HTML
const htmlStream = new ReadableStream({
async start(controller) {
controller.enqueue('<!DOCTYPE html><html>...<div id="app">')
const reader = stream.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
controller.enqueue(value)
}
controller.enqueue('</div>...</html>')
controller.close()
}
})
return new Response(htmlStream, {
headers: { 'Content-Type': 'text/html' }
})
}
}Suspense 与流式渲染
流式渲染天然支持 Suspense:
javascript
async function renderVNodeToStream(vnode, context, renderer) {
if (vnode.type === Suspense) {
const { default: content, fallback } = vnode.children
// 先发送 fallback
await renderSlot(renderer.write, fallback, context)
// 等待异步内容
try {
const asyncContent = await resolveAsyncContent(content)
// 发送替换脚本
renderer.write(`<script>replaceContent(...)</script>`)
// 发送实际内容
await renderSlot(renderer.write, asyncContent, context)
} catch (error) {
// 保持 fallback
}
return
}
// ...
}背压处理
什么是背压? 可以用水管来类比:
想象你用一根水管浇花,水龙头(服务端渲染)出水很快,但花盆(客户端网络)吸收水的速度有限。如果水龙头一直开着最大,水就会溢出来——这就是**背压(Backpressure)**问题。
在流式渲染中:
- 服务端:渲染速度可能很快
- 网络/客户端:接收处理速度可能较慢
- 背压:当接收端处理不过来时,发送端需要暂停等待
javascript
function createStreamRenderer(push, onError) {
let paused = false
let pendingResolve = null
return {
async write(content) {
// 如果被暂停,等待恢复
if (paused) {
await new Promise(resolve => {
pendingResolve = resolve
})
}
const canContinue = push(content)
if (!canContinue) {
paused = true
}
},
resume() {
paused = false
if (pendingResolve) {
pendingResolve()
pendingResolve = null
}
}
}
}性能指标
流式渲染改善的指标:
- TTFB(Time To First Byte):大幅减少
- FCP(First Contentful Paint):显著减少
- TTI(Time To Interactive):略微减少或不变
权衡:
- 无法在发送后修改已发送内容
- Head 中的 meta 信息需要提前确定
- 需要处理错误情况
本章小结
本章分析了流式渲染的实现:
- 核心优势:减少首字节时间
- API:renderToNodeStream、pipeToNodeWritable
- Web Streams:现代运行时支持
- Suspense 集成:先发送 fallback,后替换
- 背压处理:控制渲染速度
下一章将分析 SSR 中的响应式与副作用处理。