虚拟列表是只渲染可视区域及缓冲区节点、用空白占位其余内容的优化方案;直接渲染万条div会因DOM过多导致卡顿、内存飙升、掉帧;需用customElements.define封装并注意属性监听、布局时机、滚动容器样式等约束。

什么是虚拟列表,为什么不能直接用 div 渲染万条数据
直接用 for 循环生成 10000 个 div 会导致页面卡顿、内存飙升、滚动掉帧——浏览器要创建并维护上万个 DOM 节点,哪怕它们大部分不在视口内。虚拟列表的核心思路是:只渲染当前可视区域 + 少量缓冲区的节点,其余用空白占位(height 占位),靠监听 scroll 动态更新渲染范围。
用 customElements.define 封装虚拟列表组件的关键约束
自定义元素本身不解决虚拟化逻辑,但能封装滚动监听、尺寸计算和 slot 分发。必须注意三点:
-
observedAttributes中需包含item-height和item-count,否则无法响应式更新; - 不能在
connectedCallback里直接调用getBoundingClientRect(),此时元素可能未完成布局,应改用requestAnimationFrame延迟读取; - 滚动容器必须显式设置
overflow-y: auto且有固定高度,否则scrollTop无效,scrollHeight会失真。
IntersectionObserver 能替代 scroll 事件做虚拟列表吗
不能作为主方案。虽然 IntersectionObserver 可以检测元素是否进入视口,但它不提供 scrollTop 和精确的滚动偏移,无法计算「当前应显示哪几项」。它更适合做懒加载或触底加载,而非实时滚动定位。真实项目中仍需监听 scroll 或 scrollend(配合节流),再结合 scrollTop / itemHeight 算出起始索引。
示例计算逻辑:
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);<br>const endIndex = Math.min(itemCount, startIndex + visibleCount + buffer * 2);
立即学习“前端免费学习笔记(深入)”;
如何让 slot 内容正确参与虚拟滚动渲染
不能把 <slot> 直接放在滚动容器里——它会一次性全部渲染。正确做法是:在自定义元素内部用 document.createElement('div') 手动创建项容器,再用 slot.assignedNodes() 或传入 renderItem 回调函数动态填充内容。若依赖外部 slot,需配合 slotchange 事件重新触发渲染,并缓存 assignedNodes() 结果避免重复查询。
关键点:
- 滚动容器(
scroller)必须是 shadow DOM 内部元素,不能是 light DOM 的slot父级; - 每个虚拟项需设置唯一
key(如索引或数据 id),避免复用时状态错乱; - 项高度必须严格一致(或使用
itemHeight属性统一配置),变高列表需额外实现position: sticky或offsetTop映射表,复杂度陡增。
实际滚动性能瓶颈往往不在 JS 计算,而在频繁的 DOM 插入/移除和样式重排。最稳妥的做法是:用 innerHTML 批量写入、所有项用 will-change: transform 提前升层、禁用过渡动画。变高列表几乎必然要放弃纯 CSS 方案,转向 ResizeObserver + 高度缓存 + 二分查找定位,那已经超出基础自定义元素的范畴了。



















