核心解法是只渲染可见区域内容,避免一次性插入几万条DOM节点;因浏览器渲染管线过载导致卡顿,DocumentFragment可将多次layout压缩为一次触发。

直接渲染几万条数据会卡死浏览器,这不是代码写得不够好,而是浏览器根本扛不住那么多 DOM 节点。核心解法就一条:不渲染看不见的。
为什么 innerHTML 或 appendChild 一次性塞几万条必卡
浏览器不是“生成完再画”,而是边解析 DOM 边布局、边样式计算、边重排重绘。几万个节点一塞进去,主线程立刻被 DOM 构建 + 样式计算 + layout 占满,页面冻结几秒甚至崩溃。这不是 JS 慢,是渲染管线过载。
-
innerHTML = hugeString会触发完整 HTML 解析 + DOM 构建 + layout,字符串拼接本身在超 5000 条时也成瓶颈 - 每个
appendChild都可能触发重排,10000 次调用 ≈ 10000 次 layout 压力 - 即使元素为空,仅存在 DOM 树中就已消耗可观内存,DevTools 里 DOM 节点数破万是典型信号
用 DocumentFragment 批量插入比逐个 appendChild 快多少
关键不是“少调用一次”,而是把多次 layout 触发压缩为一次真实 DOM 插入。Fragment 是轻量级 DOM 文档片段,不挂载到页面,不参与 layout/paint 流程,只在最后 appendChild(frag) 时才真正触发一次渲染。
- 正确写法:
const frag = document.createDocumentFragment(); for (let i = 0; i ${i}`; frag.appendChild(tr); } table.appendChild(frag); - 别用
table.innerHTML += ...循环拼接——每次赋值都触发完整解析和重排 - Fragment 不能解决节点数量问题,它只缓解插入开销;若仍要渲染全部 10 万条,卡顿只是稍晚出现
滚动时只渲染可视区域:用 IntersectionObserver 还是手动监听 scroll
IntersectionObserver 是现代首选,轻量、异步、不阻塞主线程;getBoundingClientRect() 需手动绑定 scroll 事件,容易因频繁触发掉帧。但前者 IE 不支持,后者在老项目里仍是刚需。
立即学习“前端免费学习笔记(深入)”;
- 新项目优先用
IntersectionObserver监听列表项是否进入视口,动态append/remove节点 - 兼容 IE 时,用
getBoundingClientRect()+throttle,且只对「正在滚动中」的元素做判断,避免每次scroll都遍历全部项 - 必须给容器设固定高度和
overflow-y: auto,否则无法形成滚动上下文,视口判断失效 - 别忘了预留缓冲区(比如多渲染前后各 5 行),避免快速滑动时白屏
虚拟滚动为什么不能只靠 transform: translateY
纯 CSS 位移看似省 DOM,但所有数据仍得存在内存里,滚动越远,JS 对象越多,GC 压力大;而且无法响应用户点击某一行的真实索引(你看到的是第 2000 行,但 DOM 节点可能是第 0 个)。
- 虚拟滚动必须配合「数据映射」:维护
startIndex和visibleCount,每次滚动后重新计算并更新innerHTML或复用节点 - 复用节点比新建更快:用
Array.from({length: visibleCount})渲染固定数量的<tr>,再用 <code>dataset.rowIndex绑定真实数据下标 - 定位用
transform: translateY(${startIndex * itemHeight}px),不用top或paddingTop—— 后两者会强制重排 - 容器内需一个撑高
<div>:高度设为 <code>${totalItems * itemHeight}px,否则滚动条不会出现最容易被忽略的不是怎么写,而是忘记控制容器高度、没设 overflow、没预留缓冲区、或者把虚拟滚动当成“加个 transform 就完事”。真实场景里,DOM 复用、数据映射、节流时机、滚动容器样式,缺一不可。



















