一、为什么超大列表渲染会卡顿?

你可以想象日常开发中遇到的场景:比如电商平台的商品评论页,后台系统的十万条操作日志,或者IM工具的历史消息列表,当需要一次性渲染几十万条数据时,浏览器会被迫创建对应数量的DOM节点。每个DOM节点都要计算样式、布局,就像让一个人同时搬几万箱快递,不仅效率极低,滚动时还会因为来不及处理大量DOM更新出现掉帧、卡顿,甚至直接卡死页面。

为什么会这样?核心在于浏览器的性能瓶颈:一次性渲染过多DOM会触发大规模回流重绘,占用大量主线程资源,导致用户交互体验骤降。而要解决这个问题,最常用的方案就是虚拟滚动技术。

二、虚拟滚动到底是什么?

虚拟滚动的核心逻辑是「按需渲染DOM」—— 不需要把所有数据对应的真实DOM都创建出来,只渲染用户当前可视区域能看到的那部分DOM,当用户滚动时,再把可视区域外的旧DOM替换成新的DOM,始终让页面中的DOM数量维持在20-50个(具体数值取决于可视区域高度和单条项高度),这样就能大幅减少浏览器的性能压力,实现流畅滚动。

要实现这个核心逻辑,必须处理两个关键细节:一是滚动事件的节流(避免频繁处理滚动导致性能问题),二是动态高度的计算(如果列表项高度不固定,要精准计算偏移避免滚动跳动)。

三、具体实现步骤(原生JavaScript示例)

本次示例使用纯原生JavaScript,不需要任何框架依赖,任何人都可以直接在浏览器中运行,代码带详细注释,方便理解每一步逻辑。

1.1 准备工作:模拟百万级测试数据

先生成10万条模拟数据,每条数据的高度随机(20-60px),用来模拟实际场景中动态高度的情况:

// 技术栈:原生JavaScript(无框架依赖,可直接在浏览器运行)
// 生成10万条带动态高度的测试数据
const TOTAL_ITEMS = 100000;
const items = []; // 存储所有数据内容
const itemHeights = []; // 缓存每条数据的高度,用于后续计算
let averageHeight = 0; // 存储所有数据的平均高度,简化初始计算

// 随机生成每条数据的内容和高度
for (let i = 0; i < TOTAL_ITEMS; i++) {
  const height = Math.floor(Math.random() * 40) + 20; // 高度:20~59px之间
  items.push({ id: i, content: `第${i}条测试数据,高度${height}px` });
  itemHeights.push(height);
  averageHeight += height;
}
averageHeight = averageHeight / TOTAL_ITEMS; // 计算平均高度

1.2 搭建核心DOM结构

虚拟滚动需要两个核心容器:视口(可视区域,固定大小,隐藏溢出)内容容器(用来撑出滚动条,定位可视项)

// 挂载DOM到页面的容器(需要先在HTML中添加id为app的元素,如<div id="app"></div>)
const rootContainer = document.getElementById('app');

// 1. 视口:用户能看到的区域,固定高度,隐藏溢出
const viewport = document.createElement('div');
viewport.style.height = '500px'; // 可视区域高度可自定义
viewport.style.overflow = 'auto'; // 必须启用滚动,触发scroll事件
viewport.style.border = '1px solid #f0f0f0';
viewport.style.boxSizing = 'border-box';

// 2. 内容容器:总高度由所有数据决定,用于撑起滚动条,定位当前可视项
const contentWrapper = document.createElement('div');
contentWrapper.style.position = 'relative'; // 方便设置偏移量
contentWrapper.style.width = '100%';

// 挂载到页面
viewport.appendChild(contentWrapper);
rootContainer.appendChild(viewport);

1.3 滚动事件节流处理

原生scroll事件会在用户滚动时高频触发,每秒可达几十次,会占用大量主线程资源,所以必须用节流函数限制处理频率:

// 节流函数:限制函数执行频率,delay为间隔时间,默认16ms(适配60fps流畅度)
function throttle(fn, delay = 16) {
  let timer = null;
  return function(...args) {
    if (timer) return; // 已有延迟任务,直接返回
    timer = setTimeout(() => {
      fn.apply(this, args); // 执行目标函数
      timer = null;
    }, delay);
  };
}

1.4 核心渲染逻辑:计算可视项并更新DOM

核心是根据滚动距离,计算当前可视区域对应的是哪部分数据,然后只渲染这部分数据:

// 静态配置:可视区域能容纳的项数(根据视口高度和平均高度计算)
const VISIBLE_ITEM_COUNT = Math.ceil(500 / averageHeight);
let currentStartIndex = 0; // 当前可视区域的起始数据索引

// 渲染可视区域的项
function renderVisibleItems() {
  // 1. 获取视口的滚动距离
  const scrollTop = viewport.scrollTop || 0;

  // 2. 计算当前可视区域的起始索引(简化版,实际用动态高度需累计计算)
  currentStartIndex = Math.max(0, Math.floor(scrollTop / averageHeight));
  
  // 3. 截取可视范围内的 data 片段
  const visibleData = items.slice(currentStartIndex, currentStartIndex + VISIBLE_ITEM_COUNT);

  // 4. 更新内容容器:设置总高度(撑起滚动条,避免滚动条高度异常)
  contentWrapper.style.height = `${TOTAL_ITEMS * averageHeight}px`;
  // 5. 设置内容容器的顶部偏移,让可视项对齐视口
  contentWrapper.style.paddingTop = `${currentStartIndex * averageHeight}px`;

  // 6. 渲染可视项的DOM(带样式和内容)
  contentWrapper.innerHTML = visibleData.map(item => `
    <div style="height: ${itemHeights[item.id]}px; border-bottom: 1px solid #eee; padding: 8px;">
      ${item.content}
    </div>
  `).join('');
}

// 初始化渲染 + 绑定带节流的滚动事件
renderVisibleItems();
viewport.addEventListener('scroll', throttle(renderVisibleItems));

1.5 优化动态高度:避免滚动跳动

刚才的示例用平均高度简化了计算,实际场景中每条数据高度不同,会导致滚动跳动,需要优化累计高度计算:

// 补充:提前计算每条数据的累计高度,用于精准定位(避免滚动跳动)
const cumulativeHeight = [0]; // 第0条数据的顶部偏移是0
for (let i = 1; i < TOTAL_ITEMS; i++) {
  cumulativeHeight[i] = cumulativeHeight[i - 1] + itemHeights[i - 1];
}

// 替换renderVisibleItems中的索引计算逻辑
currentStartIndex = cumulativeHeight.findIndex((top, idx) => top > scrollTop) - 1;
// 边界处理:防止索引超出范围
currentStartIndex = Math.max(0, Math.min(currentStartIndex, TOTAL_ITEMS - VISIBLE_ITEM_COUNT));

四、该方案的优缺点

优点

  1. 性能极佳:DOM数量始终维持在几十个级别,一次性渲染的DOM极少,滚动时几乎不会卡顿;
  2. 代码简洁:原生JS即可实现,不需要引入第三方库,适合快速上手;
  3. 适配动态高度:优化累计高度后,能精准处理不同高度的列表项,不会出现滚动跳的问题;
  4. 兼容性好:原生JS方案适配所有现代浏览器,不需要额外兼容处理。

缺点

  1. 需要处理边界情况:比如滚动到底部、顶部的逻辑,以及数据不足时的空状态;
  2. 动态更新数据的成本高:如果列表数据实时变化,需要重新计算累计高度数组;
  3. 不适合极小列表:如果列表项不足几十个,虚拟滚动反而会增加不必要的逻辑,性能不如普通列表。

五、注意事项

  1. 滚动容器必须设置overflow: autoscroll,否则不会触发滚动事件;
  2. 节流间隔的选择:16ms(60fps)是流畅度和性能的平衡点,不要设置太短(会导致主线程阻塞)或太长(滚动时会有延迟);
  3. 动态高度计算:必须等所有列表项的内容加载完成(比如图片加载完成)后,再计算高度,否则累计高度会不准,导致滚动跳动;
  4. 边界处理:要处理currentStartIndex为0或TOTAL_ITEMS - VISIBLE_ITEM_COUNT的情况,避免出现空DOM或越界渲染。

六、总结

虚拟滚动技术是解决超大列表渲染性能问题的最优方案之一,核心是通过「按需渲染DOM」减少浏览器的压力,同时通过滚动节流和动态高度计算保障用户体验。本文用原生JavaScript实现了完整的虚拟滚动示例,即使是前端新手也能看懂每一步逻辑,适配电商评论、后台日志、IM历史消息等各种超大列表场景。你可以根据实际需求调整可视区域高度、节流间隔等参数,轻松实现流畅的长列表滚动效果。