流式 ssr 是服务端边渲染边 flush 字节块到 http 响应流,需设响应头为 text/html; charset=utf-8 且保持连接;rendertopipeablestream 支持 suspense 流式切片,而 rendertostring 同步阻塞导致高 ttfb。

流式 SSR 不是“把 HTML 拆成字符串发”,而是让 renderToPipeableStream 或 renderToReadableStream 在服务端边生成、边 flush 字节块到 HTTP 响应流中——关键在于响应头必须设为 text/html; charset=utf-8,且不能提前结束连接。
为什么用 renderToPipeableStream 而不是 renderToString
因为 renderToString 是同步阻塞的:它等所有组件(包括 Suspense 边界内异步数据)全部 resolve 后才返回完整 HTML 字符串,TTFB 高、首屏卡顿明显。而 renderToPipeableStream 返回一个可挂起的流对象,遇到 <suspense fallback></suspense> 时立即输出 fallback 内容并继续往下流式发送已就绪部分,后续数据就绪后通过 <script></script> 注入或流式 patch 补充。
- 必须搭配 React Server Components(RSC)或
async组件 +Suspense才能触发流式切片 - 不支持在流式过程中动态修改
document.title或插入全局<meta>——这些必须写在初始 HTML 的里 - 流式输出期间若发生未捕获错误,整个响应会中断;需用
stream.onError显式监听并 fallback 到完整降级渲染
res.write() 和 res.flush() 在 Node.js 中怎么配合流式 SSR
Node.js 的 http.ServerResponse 默认启用 chunked encoding,但必须手动调用 res.flush()(或 res.write(chunk, cb) 的回调触发)才能真正把缓冲区内容推给客户端。光靠 stream.pipe(res) 不够——尤其在 Express 中,中间件可能拦截或延迟 flush。
文章转信息图。将文章/笔记转化为手机可读的 HTML 信息图,自动匹配视觉风格。触发场景:文章转图、笔记转图、信息图、转小红书图、做张图、可视化这篇文章、文生图。
- Express 用户需禁用
res.send(),改用res.status(200).set({ 'Content-Type': 'text/html; charset=utf-8', 'X-Content-Type-Options': 'nosniff' }).write(htmlStart)开头,再 pipe 流 - 若用
renderToReadableStream,需确保 Node.js 版本 ≥ 18.18.0,且调用stream.getReader()+reader.read()循环写入,每次res.write(chunk.value)后紧跟res.flush() - nginx 等反向代理默认缓冲前 1KB 响应体,需配置
proxy_buffering off或proxy_buffer_size 1k避免阻塞首块传输
浏览器收到流式 HTML 后如何解析和渲染
浏览器 HTML 解析器是流式的:它边接收字节、边 token 化、边构建 DOM。只要第一个 标签已到达,解析就启动;遇到 <script></script> 仍会阻塞,但 <link rel="stylesheet"> 和图片等资源请求可并行发起。
- 流式 HTML 中不能有未闭合的
<script></script>或<style></style>——否则解析器会等待后续字节,导致“卡住”现象 - React 注入的 hydration 脚本(如
hydrateRoot)必须放在流式 HTML 的末尾,且依赖document.body已存在;若流式中途断开,脚本不会执行 - DevTools 的 Network → Response 标签页里能看到分块响应(多个小 size 的 chunk),但 Elements 面板只显示最终合并后的 DOM ——想验证流式效果得用 curl +
--no-buffer或 Wireshark 抓包
真正难的不是开启流式,而是控制流式切片的粒度:太细(比如每个 <div> 都 flush)会增加 TCP 包开销;太粗(只分 header/body 两块)又失去意义。实际项目中,粒度基本由 <code>Suspense 边界位置决定,而边界是否合理,取决于数据加载的自然分组——这点容易被忽略。










