直接 innerHTML += 会破坏协同渲染一致性,因为它不保留节点引用、不校验 DOM 状态、不感知版本冲突,导致多人编辑时覆盖彼此变更、丢失事件监听器与状态信息。

为什么直接 innerHTML += 会破坏协同渲染一致性
大型文档管理系统里,多人同时编辑同一 HTML 片段时,用 innerHTML += 或 element.insertAdjacentHTML() 是最常见也最危险的操作。它不保留已有节点引用、不校验 DOM 状态、不感知版本冲突,结果就是:A 用户插入一段 <li>,B 用户紧接着插入另一段,但 B 的操作覆盖了 A 的 DOM 结构,且浏览器不会报错——只默默丢弃 A 的节点。
- 所有事件监听器、
dataset状态、焦点/选区信息全部丢失 - 服务端若未做 OT/CRDT 合并,客户端看到的只是“最后写入者胜利”,不是协同
- 含
<table>或嵌套<details>的结构极易被浏览器自动补全,导致<tbody>被挪到错误位置
必须用 template.content.cloneNode(true) 替代字符串拼接
真正安全的动态注入,不是往现有 DOM 里塞字符串,而是把新内容封装进 <template>,再克隆使用。这是唯一能跳过重复解析、避免结构污染的方式。
-
template.content是只读DocumentFragment,直接appendChild(template.content)会“搬走”内容,第二次调用返回空片段——静默失败,极难排查 - 必须用
template.content.cloneNode(true)获取可操作副本;importNode也可,但兼容性略差 - 克隆后若含重复
id="editor-cursor",后续document.getElementById()只取第一个,需主动重写 ID 或改用data-id - 克隆节点上无法直接绑定事件,得插入 DOM 后再查子元素、再监听,不能在克隆前调用
querySelector
协作场景下 DocumentFragment 不是万能解药
很多人以为用 DocumentFragment 就能解决性能与协同问题,其实它只解决“插入前”的批量操作,对插入后的状态同步毫无帮助。
-
fragment插入后自动清空,不能复用;每次更新都得重新createDocumentFragment() - 它不支持
querySelector,也不保存事件监听器;含<input>或<textarea>时,插入瞬间触发 focus/selection 同步,光标跳变 - 若服务端已输出 SSR 内容(比如预渲染的文档大纲),前端用
<template>补充正文,ID、data-state、表单值这些状态不会自动 hydrate,必须显式接管或重置 - 交互型控件(如可折叠章节、带撤销栈的富文本块)应走 DOM 复用池,而非 Fragment 临时组装
非首屏内容必须配合 IntersectionObserver 按需激活
门户网站级文档常含几十页内容,一次性渲染所有 HTML 会导致内存暴涨、主线程卡死。但简单用 display: none 并不能解决问题——它仍参与 CSSOM 构建和 layout 计算。
立即学习“前端免费学习笔记(深入)”;
- 正确做法:把非首屏 HTML 封装进
<template>,初始不插入 DOM - 用
IntersectionObserver监听进入视口,触发时才克隆并插入,同时触发 hydration(如恢复光标位置、重连 WebSocket 订阅) - 表格类模块务必用显式
<tbody>包裹动态行,克隆后 append 到现有<tbody>,而非整个<table> - 批量插入多个节点时,先塞进
DocumentFragment,再一次性appendChild,避免反复 layout



















