VirtualList 虚拟列表

虚拟列表组件用于高效渲染大量数据。通过只渲染可视区域内的项目,可以轻松处理数万甚至数十万条数据而不影响性能。

特性

  • ✅ 支持定高和不定高两种模式
  • ✅ 自动计算可视区域,只渲染可见项
  • ✅ 缓冲区机制,减少滚动时的闪烁
  • ✅ 不定高模式支持动态高度测量和缓存
  • ✅ 支持滚动到指定索引
  • ✅ SSR 兼容

定高模式

当所有项目高度相同时,使用定高模式可以获得最佳性能。

Item 0
这是第 0 项
Item 1
这是第 1 项
Item 2
这是第 2 项
Item 3
这是第 3 项
Item 4
这是第 4 项
Item 5
这是第 5 项
Item 6
这是第 6 项
Item 7
这是第 7 项
Item 8
这是第 8 项
Item 9
这是第 9 项
Item 10
这是第 10 项
Item 11
这是第 11 项
Item 12
这是第 12 项
Item 13
这是第 13 项
Item 14
这是第 14 项
Item 15
这是第 15 项
Item 16
这是第 16 项
Item 17
这是第 17 项
import { VirtualList } from '@enterprise-ui/react19';

const items = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: `Item ${i}`,
}));

<VirtualList
  items={items}
  height={400}
  itemHeight={60} // 固定高度
  renderItem={(item) => (
    <div className="p-4 border-b">
      {item.name}
    </div>
  )}
/>

不定高模式

当项目高度不一致时,使用不定高模式。组件会自动测量和缓存每个项目的高度。

Item 0
这是一段描述文字 这是一段描述文字 这是一段描述文字
额外信息
Item 1
这是一段描述文字 这是一段描述文字 这是一段描述文字 这是一段描述文字
Item 2
这是一段描述文字
额外信息
Item 3
这是一段描述文字
额外信息
Item 4
这是一段描述文字 这是一段描述文字
额外信息
Item 5
这是一段描述文字
Item 6
这是一段描述文字 这是一段描述文字 这是一段描述文字
额外信息
Item 7
这是一段描述文字 这是一段描述文字 这是一段描述文字 这是一段描述文字 这是一段描述文字
Item 8
这是一段描述文字 这是一段描述文字 这是一段描述文字 这是一段描述文字 这是一段描述文字
额外信息
Item 9
这是一段描述文字 这是一段描述文字 这是一段描述文字
import { VirtualList } from '@enterprise-ui/react19';

const items = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: `Item ${i}`,
  description: '描述文字',
}));

<VirtualList
  items={items}
  height={400}
  estimatedItemHeight={80} // 预估高度
  renderItem={(item) => (
    <div className="p-4 border-b">
      <div className="font-medium">{item.name}</div>
      <div className="text-sm">{item.description}</div>
    </div>
  )}
/>

滚动到指定位置

可以通过 scrollToIndex 属性滚动到指定索引。

Item 0
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Item 13
Item 14
Item 15
Item 16
Item 17
const [scrollToIndex, setScrollToIndex] = useState<number>();

<VirtualList
  items={items}
  height={300}
  itemHeight={50}
  scrollToIndex={scrollToIndex}
  renderItem={(item) => <div>{item.name}</div>}
/>

<button onClick={() => setScrollToIndex(5000)}>
  滚动到第 5000 项
</button>

实现原理详解

1. 定高模式原理

定高模式是最简单高效的虚拟列表实现方式。当所有项目高度相同时,可以直接通过数学计算确定可视区域。

核心算法:

// 1. 计算可视区域起始索引
const startIndex = Math.floor(scrollTop / itemHeight);

// 2. 计算可视区域结束索引
const visibleCount = Math.ceil(containerHeight / itemHeight);
const endIndex = Math.min(
  startIndex + visibleCount + overscan * 2,
  totalItems - 1
);

// 3. 计算总高度和偏移量
const totalHeight = totalItems * itemHeight;
const offsetY = startIndex * itemHeight;

// 4. 只渲染可见项
const visibleItems = items.slice(startIndex, endIndex + 1);

性能分析:

  • 时间复杂度:O(1) - 直接计算,无需遍历
  • 空间复杂度:O(visibleCount) - 只存储可见项
  • DOM 节点数:visibleCount + overscan * 2(通常 < 50 个)
  • 滚动性能:60fps,无卡顿

关键优化点:

  • 使用 transform: translateY() 而非 top,利用 GPU 加速
  • 缓冲区机制:上下各渲染 overscan 个项目,减少滚动时的空白
  • 避免频繁的 DOM 操作,只在索引变化时更新

2. 不定高模式原理

不定高模式更复杂,因为需要动态测量每个项目的高度。核心思路是:高度缓存 + 位置计算 + 二分查找。

核心数据结构:

// 高度缓存 Map<index, height>
const heightCache = new Map<number, number>();

// 位置缓存 Map<index, offset>
const offsetCache = new Map<number, number>();

核心算法:

// 1. 计算累计高度(位置)
function getItemOffset(index: number): number {
  // 如果缓存中有,直接返回
  if (offsetCache.has(index)) {
    return offsetCache.get(index)!;
  }
  
  // 否则计算累计高度
  let offset = 0;
  for (let i = 0; i < index; i++) {
    offset += heightCache.get(i) || estimatedHeight;
  }
  
  // 缓存结果
  offsetCache.set(index, offset);
  return offset;
}

// 2. 二分查找起始索引(优化查找)
function findStartIndex(scrollTop: number): number {
  let left = 0;
  let right = items.length - 1;
  let result = 0;
  
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    const offset = getItemOffset(mid);
    
    if (offset < scrollTop) {
      left = mid + 1;
      result = mid;
    } else {
      right = mid - 1;
    }
  }
  
  return Math.max(0, result - overscan);
}

// 3. 查找结束索引
function findEndIndex(startIndex: number, scrollTop: number, containerHeight: number): number {
  const startOffset = getItemOffset(startIndex);
  let currentOffset = startOffset;
  let index = startIndex;
  
  while (index < items.length && currentOffset < scrollTop + containerHeight) {
    const itemHeight = heightCache.get(index) || estimatedHeight;
    currentOffset += itemHeight;
    index++;
  }
  
  return Math.min(items.length - 1, index + overscan);
}

高度测量策略:

  • 懒加载测量:只在项目进入可视区域时测量高度
  • Ref 回调:使用 React ref 获取 DOM 元素的实际高度
  • 缓存机制:测量后的高度存入 Map,避免重复测量
  • 预估高度:未测量的项目使用预估高度,减少布局抖动
// 高度测量实现
useEffect(() => {
  visibleItems.forEach(({ index }) => {
    const element = itemRefs.current.get(index);
    if (element && !heightCache.has(index)) {
      // 测量实际高度
      const measuredHeight = element.offsetHeight;
      heightCache.set(index, measuredHeight);
      
      // 清除位置缓存(因为高度变化会影响后续位置)
      clearOffsetCacheFrom(index);
    }
  });
}, [visibleItems]);

性能优化策略:

  • 二分查找:O(log n) 时间复杂度查找起始索引
  • 位置缓存:缓存累计高度,避免重复计算
  • 增量更新:高度变化时只更新受影响的位置缓存
  • 防抖测量:避免频繁的 DOM 测量操作

性能对比:

模式时间复杂度空间复杂度首次渲染滚动性能
定高模式O(1)O(visibleCount)极快60fps
不定高模式O(log n)O(n) 缓存需要测量60fps(缓存后)

3. 滚动优化

滚动事件处理:

// 使用 requestAnimationFrame 优化滚动
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
  const newScrollTop = e.currentTarget.scrollTop;
  
  // 使用 requestAnimationFrame 确保在下一帧更新
  requestAnimationFrame(() => {
    setScrollTop(newScrollTop);
    onScroll?.(newScrollTop);
  });
}, [onScroll]);

避免布局抖动:

  • 使用 transform 而非改变 top/left,避免重排
  • 预估高度减少首次渲染时的布局变化
  • 缓冲区机制平滑滚动体验

性能优化

  • 定高模式:O(1) 时间复杂度计算位置,性能最优
  • 不定高模式:使用高度缓存和二分查找,O(log n) 时间复杂度
  • 缓冲区:上下各渲染 overscan 个项目,减少滚动时的闪烁
  • 懒加载高度:不定高模式只在需要时测量高度

API

VirtualList Props

参数说明类型默认值
items数据列表T[]-
renderItem渲染函数(item: T, index: number) => React.ReactNode-
height容器高度number-
itemHeight每项高度(定高模式)number-
estimatedItemHeight预估高度(不定高模式)number50
overscan缓冲区大小number5
scrollToIndex滚动到指定索引number-
onScroll滚动事件回调(scrollTop: number) => void-
className自定义类名string-

注意事项

  • 定高模式性能最优,如果可能尽量使用定高模式
  • 不定高模式首次渲染可能需要测量高度,会有轻微延迟
  • 建议设置合理的 overscan 值,平衡性能和体验
  • 组件已标记 'use client',需要在客户端组件中使用