直接用domparser解析50mb html会oom,根本原因是其必须构建完整dom树导致v8堆内存瞬间吃满;实操应改用htmlparser2流式解析,禁用xmlmode,仅监听opentag/closetag事件,并注入chunk-meta注释保留上下文。

为什么直接用 DOMParser 解析 50MB HTML 会 OOM
根本原因是 DOMParser 必须构建完整 DOM 树,V8 堆内存瞬间吃满。这不是配置问题,而是设计边界——网关层不负责渲染,只做结构识别。遇到含千级嵌套 <div> 的老系统导出页时,<code>RangeError: Maximum call stack size exceeded 是递归下降解析器栈溢出的典型表现。
实操建议:
- 禁用
xmlMode: true,HTML 文档必须用xmlMode: false,否则自闭合标签如<img>会被错误解析 - 用
htmlparser2.Parser+ 自定义回调,只监听opentag和closetag,跳过文本拼接和树构建 - 在
opentag中用栈记录深度(如["h1", "section", "article"]),closetag时弹出,避免深递归 - 文本内容不缓存为字符串,改用
Buffer或Uint8Array流式暂存,按需截断
如何让浏览器边收边渲染但不卡死
关键不是“发得快”,而是每块都语法合法。浏览器解析器不是拼图游戏,它不会等下一块补全标签。一个没闭合的 <div> 在 Safari 上可能直接卡住,Chrome 也可能延迟渲染直到超时。<p>实操建议:</p>
<ul>
<li>每个 <code>res.write() 必须输出最小可插入 DOM 的闭合结构:比如 <p>文字</p>、<section><h2>标题</h2>
<p>内容</p></section>
res.write('<div class="') 是错的;应合并为 <div class=" card>
<li>首块必须包含完整启动帧:<code><meta charset="utf-8">
charset=utf-8,否则含 BOM 的模板文件会导致乱码切片后怎么保证锚点跳转和上下文还原
把 30MB HTML 拆成 200 个 <section></section> 后直接转发,下游无法定位原始位置——用户点击 <h3 id="api-auth"></h3>,收到的却是无 ID 的纯片段,跳转失效。
实操建议:
- 在每个切片前注入轻量元数据注释:
<!-- chunk-meta: {"src":"help.html","offset": 12480,"headers": ["h1","h2","h3"],"id":"api-auth"} --> -
offset字段用于下游反查原始文件偏移,支撑日志定位与调试 - 若下游是 SSR 服务,可提取
id构建客户端跳转链接;若是向量化服务,headers数组可直接喂给 embedding 模型作上下文提示 - 元数据必须用 HTML 注释包裹,确保不干扰 CSS/JS 执行
流式分发时内存堆积怎么防
网关不是中转站,而是流量调节阀。把 50MB HTML 拆成 64KB 块直接 pipe() 给下游,一旦后端消费慢(如 Java Spring Boot 默认 servlet 缓冲区仅 8KB),未读 chunk 就会滞留在 Node.js Writable 内部缓冲区,引发内存堆积。
实操建议:
- 显式控制
WritableStream的highWaterMark,例如设为64 * 1024 - 监听
drain事件,在缓冲区快满时暂停写入,等下游消费后再恢复 - 分块逻辑不可下放到 CDN 边缘——CDN 不理解语义块,也无法注入
chunk-meta - 禁用
Content-Length,靠Transfer-Encoding: chunked驱动底层分块,不要手动设这个 header
真正难的不是切,是切完还能让下游知道“这是哪一段”。chunk-meta 注释看着轻,但它要对齐原始文件偏移、兼容 SSR 跳转、喂给 embedding 模型——三者字段含义和使用方式完全不同,漏掉任一环,流式就退化成裸 HTTP 分块。











