直接渲染百万条会卡死是因为dom节点过多导致主线程被layout和paint塞满,内存超1.5gb,滚动帧率骤降;应采用虚拟滚动,固定行高、position: absolute占位、requestanimationframe节流等优化策略。

为什么- 直接渲染百万条会卡死
不是浏览器不行,是 DOM 节点数量突破临界点后,主线程被 layout 和 paint 塞满。100 万 <li> 意味着至少 100 万个 DOM 对象、样式树、布局树全量维护——内存占用轻松破 1.5GB,scrollTop 读取一次就触发强制同步布局,滚动帧率掉到个位数是常态。
常见错误现象:RangeError: Maximum call stack size exceeded(递归过深)、DevTools Performance 面板里 Layout 占比超 70%、滚动时鼠标指针延迟半秒才响应。
- 别用
v-for或map()直接遍历生成全部<li>,Vue/React 默认不复用节点,重建开销极大 - 禁用
display: none或visibility: hidden做“伪虚拟”——节点还在内存里,reflow 照样发生 - 避免用
offsetHeight计算容器高度,它会触发重排;改用clientHeight或 CSS 固定height
固定行高 + scrollTop 算索引是最稳路径
动态高度方案看着灵活,但每次滚动都要调用 getBoundingClientRect() 缓存位置,而这个 API 在快速滚动中反复触发,等于主动制造 layout thrashing。90% 场景下,把 <li> 行高设为固定值(比如 48px)就能解决绝大多数问题。
关键计算逻辑必须用:
-
startIndex = Math.floor(scrollTop / itemHeight)—— 注意用Math.floor,不用parseInt,因为scrollTop是小数 -
visibleCount = Math.ceil(container.clientHeight / itemHeight) + 4——+4是缓冲区,防快速滚动时白屏 - 结束索引要兜底:
endIndex = Math.min(startIndex + visibleCount, totalItems - 1)
行高偏差超过 ±5px 就会出现视觉跳动;若业务真无法固定,优先在服务端注入 height 字段,而不是前端实时测量。
position: absolute 占位比 transform: translateY() 更可控
很多教程推荐 transform: translateY(),但它在 Chrome 115–122 中会意外创建额外合成层,GPU 内存暴涨,反而更卡。真实生产环境里,position: absolute + top 是更稳妥的选择。
- 外层容器必须设
style.overflow = 'auto'且有明确height或max-height - 内部占位
<div> 要加 <code>position: relative,否则absolute子元素会相对于 body 定位 - 每个
<li>加position: absolute; top: ${index * itemHeight}px,不要用innerHTML = ''全量替换,改用replaceChildren()清空再 append - 别用
throttle(16),优先用requestAnimationFrame包一层:只有浏览器准备绘制下一帧时才执行处理逻辑 - 监听前先缓存
container.clientHeight和itemHeight,避免在回调里重复读取 DOM 属性 - resize 时必须重新计算
visibleCount并重置startIndex,否则缩放窗口后列表错位
滚动监听必须用 requestAnimationFrame 节流
scroll 事件在 Chrome/Firefox 中每秒可触发上百次,不节流等于让 JS 每秒执行几十次 DOM 查询 + 计算 + 更新。哪怕只做 scrollTop 读取,也足以让帧率崩盘。
最容易被忽略的是键盘导航支持——PageDown、Home、End 触发的滚动不会走 mousewheel 流程,得单独监听 keydown 并手动更新 scrollTop,否则用户一按键盘就卡住。











