真正高性能的列表渲染必须用 template.content.cloneNode(true) + DocumentFragment;template.innerHTML 永远为空,因内容被封装为只读 DocumentFragment;cloneNode(true) 是必须操作,否则导致状态丢失;填值应区分类型:纯文本用 textContent,表单用 value,富文本需转义后使用。

直接用 innerHTML 拼接列表,性能差、易 XSS、丢事件、破坏焦点——这不是写法问题,是底层机制决定的;真正高性能的列表渲染,必须走 template.content.cloneNode(true) + DocumentFragment 这条路径。
为什么 template.innerHTML 总是空字符串?
浏览器把 <template> 内容封装成只读的 DocumentFragment,它不参与 DOM 树,也不暴露字符串接口:template.innerHTML 永远返回空字符串,template.children 是空 NodeList,template.textContent 只有换行和纯文本。所有“想从 template 里取 HTML 字符串”的尝试都会失败。
唯一合法入口只有 template.content —— 它是 DocumentFragment 类型,不能直接插入,也不能直接绑定事件,但它是后续所有操作的起点。
- ❌ 错误:
document.querySelector('template').innerHTML→ 空字符串 - ❌ 错误:
tmpl.children[0]→ undefined - ✅ 正确:
tmpl.content.cloneNode(true)→ 可操作副本
cloneNode(true) 不是可选,是必须
浅克隆(cloneNode(false))或直接移动(appendChild(tmpl.content))会导致第二次渲染空白、表单状态丢失、光标消失、所有项显示相同数据——这些静默失败很难 debug。
立即学习“前端免费学习笔记(深入)”;
cloneNode(true) 才能复制完整子树、保留 input.value 当前值、select.selectedIndex、已绑定的事件监听器,以及文本节点内容。
- ❌ 错误:缓存
const content = tmpl.content后反复克隆 → 第二次起content已为空 - ❌ 错误:用
cloneNode(false)→ 只剩顶层容器,内部结构全丢 - ✅ 正确:每次循环都重新调用
tmpl.content.cloneNode(true)
填值别碰 innerHTML,优先用 textContent 和 value
用 innerHTML 填用户输入等于主动引入 XSS 风险,还会清空模板里已有的事件监听器(比如按钮上的 onclick 或 JS 中 addEventListener 绑定的回调)。
填值逻辑必须按内容类型区分:
- 纯文本:用
el.querySelector('.title').textContent = data.title - 表单控件:用
el.querySelector('input').value = data.inputValue(不是textContent) - 需渲染富文本:先对输入做 HTML 转义(如
text.replace(/&/g, '&').replace(/, '<')),再塞进innerHTML - 模板含
<script>或<style>?克隆后它们依然不执行、不生效——这是惰性设计,别指望“活”过来
批量插入必须走 DocumentFragment
循环里每轮都 container.appendChild(cloned),浏览器就得反复重排重绘。100 条数据就可能卡顿,500 条基本卡死。
正确做法是用 DocumentFragment 做轻量中转:
- ✅ 先
const frag = document.createDocumentFragment() - ✅ 循环内:克隆 → 填值 →
frag.appendChild(cloned) - ✅ 循环结束后,只调一次
container.appendChild(frag)
frag 不在主 DOM 树中,不能被 querySelector 查到,也不触发 layout/paint,但它能安全承载任意数量的节点——这才是批量插入的唯一合理路径。



















