流式输出html时不能直接用domparser或jsdom,因其会构建完整dom树导致oom;应使用htmlparser2流式解析,配合chunk-meta注释保留锚点,严格控制writablestream缓冲与sse响应格式。

流式输出 HTML 时为什么不能直接用 DOMParser 或 jsdom
网关层解析 50MB HTML 时若调用 DOMParser 或加载 jsdom,V8 引擎会尝试构建完整 DOM 树,内存瞬间暴涨甚至触发 OOM——这不是配置问题,是架构层面的不可行。高性能网关只做结构识别与路由,不渲染、不执行 JS、不维护节点关系。
- 典型错误:解析含千级嵌套
<div> 的老系统导出页,报 <code>RangeError: Maximum call stack size exceeded,本质是递归下降解析器栈溢出 - 正确做法:用
htmlparser2.Parser配合自定义回调,只监听opentag和closetag事件,跳过文本拼接和树构建 - 必须设
xmlMode: false,否则对<img>这类自闭合标签解析失败 - 若需保留语义层级(如识别
<h2></h2>下所有段落),在opentag中用栈记录深度,closetag时弹出,避免深递归 - 解决方案:每个切片前注入轻量元数据注释,格式为
<!-- chunk-meta: {"src":"help.html","offset": 12480,"headers": ["h1","h2","h3"],"id":"api-auth"} --> -
offset字段用于日志反查原始文件偏移,调试或链路追踪时关键 - 下游是 SSR 服务时,可提取
id构建客户端跳转链接;若是向量化服务,headers数组可直接喂给 embedding 模型作为上下文提示 - 注释包裹确保不干扰 CSS/JS 执行,浏览器忽略,服务端可安全提取
- 必须显式设置
highWaterMark,建议值 ≤ 64KB,防止突发写入压垮内存 - 监听
drain事件,在缓冲区快满时暂停写入,等下游消费后再恢复 - 分块逻辑绝不能下放到 CDN 边缘,CDN 不具备 HTML 语义解析能力,也无法注入
chunk-meta - 连接需设 TTL,避免长连接空闲堆积,推荐 30s 自动断连 + 客户端重连机制
- 必须三连响应头:
Content-Type: text/event-stream; charset=utf-8、Cache-Control: no-cache、Connection: keep-alive -
X-Accel-Buffering: no(Nginx)或proxy_buffering off(Apache)必须配,否则代理层缓存整块再吐出,破坏流式语义 - 每条数据严格按格式:
data: <h2>标题</h2> <p>内容</p>\n\n,注意末尾两个换行符缺一不可 - 不要混用
res.write()和res.end()在同一响应中,SSE 要求连接保持打开直到显式res.end()
HTML 分块后如何保证下游能还原锚点与上下文
把一个帮助文档切成 200 个 <section></section> 块直接转发,下游无法定位原始位置——比如用户点击 <h3 id="api-auth"></h3>,但收到的只是无 ID 的纯片段,跳转失效。
流式分发时 WritableStream 的缓冲控制要点
网关不是透明管道,而是流量调节阀。把 50MB HTML 拆成 64KB 块直接 pipe() 给下游,极易因消费速度不匹配导致内存堆积——尤其下游是 Java Spring Boot,默认 servlet 缓冲区仅 8KB,未读 chunk 会滞留在 Node.js Writable 内部缓冲区。
SSE 流式响应头与数据格式踩坑点
前端用 EventSource 接收流式 HTML 时,后端响应头错一个就收不到数据,且浏览器不会报明显错误,只会静默失败。
流式输出 HTML 的核心难点不在“怎么切”,而在“切完怎么让下游知道它在哪、怎么用”。语义锚点、缓冲水位、代理穿透这三点漏掉任何一个,都会让流式变成假流式——看着在动,实际卡在某一层。











