虚拟滚动能根治长列表卡顿,因为它只渲染视口内及缓冲区的20–50个DOM节点,通过固定容器高度、transform位移模拟滚动、节流scroll监听,避免重排重绘,将单帧耗时控制在16ms内。

为什么直接循环 appendChild 会卡死页面
因为浏览器主线程被 JS 占满,DOM 插入触发连续重排重绘,且没有机会响应滚动、点击等事件。哪怕只插入 1000 个 div,用户也会明显感知到页面“冻结”。关键不是 DOM 总数,而是单次同步操作耗时是否超过 16ms(一帧)。一旦超限,就掉帧、卡顿、白屏。
常见错误现象:for (let i = 0; i 执行完才渲染,期间 UI 完全无响应。
- 不要在单次函数调用中批量操作超过 ~200 个节点(具体阈值取决于节点复杂度)
- 避免使用
innerHTML +=拼接大量 HTML 字符串——每次赋值都会触发完整解析 + 重排 - 别依赖
setTimeout(fn, 0)做“分批”,它调度不精准,容易堆积宏任务,反而加剧卡顿
requestAnimationFrame 是更靠谱的分批入口
requestAnimationFrame 不是“动画专用”,而是浏览器承诺“在下一帧绘制前执行回调”的机制。它天然对齐渲染节奏,让 DOM 插入与浏览器绘制协同,实现真正无感知。
实操要点:
立即学习“前端免费学习笔记(深入)”;
- 每次回调只插入 10–50 个节点(简单
div可设 40;含图片/样式/事件绑定的建议 ≤20) - 必须检查是否还有剩余数据,再递归调用
requestAnimationFrame,不能用setTimeout替代 - 插入前用
document.createDocumentFragment()缓存一批节点,最后一次性 append 到父容器,减少重排次数
示例核心逻辑:
function renderBatch() {
const fragment = document.createDocumentFragment();
for (let i = 0; i < batchSize && currentIndex < total; i++) {
const div = document.createElement('div');
div.textContent = data[currentIndex++];
fragment.appendChild(div);
}
container.appendChild(fragment);
if (currentIndex < total) {
requestAnimationFrame(renderBatch);
}
}
batchSize 设多大才算“无感知”
没有固定值,取决于节点结构和设备性能。但可按以下方式快速试出安全值:
- 在目标最低机型(如低端安卓机)上打开 DevTools → Performance 面板,录制一次完整分批过程
- 观察每帧耗时:若某帧 Layout / Paint 超过 10ms,说明该
batchSize已偏高 - 从 20 开始测试,逐步加到 40、60,直到出现连续两帧 >12ms 就回退一级
- 注意:含
img或复杂 CSS 的节点,batchSize应比纯文本低 50% 以上
别迷信“越大越快”——批量过大,单帧压力飙升;过小,则调度开销占比上升,总耗时反而增加。
比“分批渲染”更彻底的解法:虚拟滚动必须考虑
当列表项超过 2000 条,或用户频繁滚动、搜索、筛选时,分批渲染只是延缓卡顿,不是根治。此时必须转向虚拟滚动。
它不靠“慢慢塞”,而是只维持视口内 + 少量缓冲区的 DOM(通常 20–50 个),滚动时动态替换内容和位置。关键点:
- 容器需设固定高度,内部用
transform: translateY()模拟滚动位移,避免触发布局计算 - 必须监听
scroll事件并节流(requestIdleCallback或throttle),否则高频触发导致主线程过载 - 首次渲染仍需分批加载(比如先渲染首屏 20 条),否则初始白屏时间太长
- 不要手写全套——用
react-window或原生virtual-scroller等成熟方案,它们已处理好边界、键盘导航、焦点管理等细节
最容易被忽略的是:虚拟滚动要求所有列表项高度一致(或提供精确高度映射表),否则滚动错位、空白、跳变会立刻暴露。



















