虚拟列表是一种前端性能优化方案,其核心思想是通过只渲染可视区域或者加上缓冲区部分区域的 DOM 元素,其他部分通过空占位符维持滚动条高度,以达到性能优化的目的

简化版:

结构:

  • 外层容器,需要拿到这个元素的srollTop,即隐藏容器的顶部,距离外层容器顶部的距离
  • 隐藏容器:需要用隐藏容器设置为列表的实际高度(item_height * count) 来撑开外层容器,以达到出现滚动条的目的
  • 实际渲染列表容器: 拿到计算好的 startIndex 以及 endIndex,仅渲染这部分的列表,再通过计算出偏移量(即列表容器本应该在隐藏容器的顶部,需要偏移到可视区域) 设置transform: transtateY(offset) 来将容器移动到可视区域
import React, { useState, useRef, useCallback, useMemo } from 'react';
 
/**
 * VirtualList 组件
 * @param {Array} items - 列表数据源
 * @param {number} itemHeight - 每个列表项的固定高度
 * @param {number} containerHeight - 容器的可视高度
 */
function VirtualList({ items, itemHeight = 50, containerHeight = 600 }) {
  const listRef = useRef(null);
  const [scrollTop, setScrollTop] = useState(0);
 
  // 处理滚动事件,记录当前的滚动距离
  const handleScroll = useCallback(() => {
    if (listRef.current) {
      setScrollTop(listRef.current.scrollTop);
    }
  }, []);
 
  // --- 核心计算逻辑 ---
  
  // 1. 计算可视区域内可以显示多少个元素,额外多加 2 个作为缓冲区(Buffer)防止白屏
  const visibleCount = useMemo(() => {
    return Math.ceil(containerHeight / itemHeight) + 2;
  }, [containerHeight, itemHeight]);
 
  // 2. 根据滚动距离,计算当前应该从第几个元素开始渲染
  const startIndex = Math.floor(scrollTop / itemHeight);
  
  // 3. 计算结束索引
  const endIndex = Math.min(items.length, startIndex + visibleCount);
 
  // 4. 获取当前需要渲染的数据切片
  const visibleItems = items.slice(startIndex, endIndex);
 
  // 5. 计算偏移量:让渲染区域始终跟随滚动条向下移动
  const offsetTop = startIndex * itemHeight;
 
  // --- 样式定义 ---
  const containerStyle = {
    height: `${containerHeight}px`,
    overflowY: 'auto',
    position: 'relative',
    border: '1px solid #ccc'
  };
 
  const phantomStyle = {
    height: `${items.length * itemHeight}px`, // 总高度,撑起滚动条
    position: 'absolute',
    left: 0,
    top: 0,
    right: 0,
    zIndex: -1
  };
 
  const listContentStyle = {
    transform: `translateY(${offsetTop}px)`, // 偏移量,将内容移动到可视区
    width: '100%'
  };
 
  return (
    <div ref={listRef} onScroll={handleScroll} style={containerStyle}>
      {/* 幻影层:负责撑开滚动条 */}
      <div style={phantomStyle} />
      
      {/* 真实渲染层:只渲染可见的部分 */}
      <div style={listContentStyle}>
        {visibleItems.map((item, index) => (
          <div
            key={startIndex + index}
            style={{ 
              height: `${itemHeight}px`, 
              lineHeight: `${itemHeight}px`,
              borderBottom: '1px solid #eee',
              boxSizing: 'border-box'
            }}
          >
            {item}
          </div>
        ))}
      </div>
    </div>
  );
}
 
export default VirtualList;

不定高方案

虚拟列表本来采用的是静态高度+直接计算偏移的方式,而在大模型输出文字流的场景当中,本身文本流是不定高的。此时就要采用 预估高度 + 动态偏移 的原理。

  • 给每个 Item 一个预估高度(Estimated Height)。
  • 渲染后通过 ResizeObserverref 获取真实高度并缓存。
  • 动态更新后续所有元素的 top 偏移量和滚动条的总高度。

大数锚点方案

大数锚点 是一个虚拟列表解决视口跳动的方案,具体参考元点引擎的那个项目是怎么写的。