全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-16 10 分钟 ✍️ juanwangdev

虚拟列表与大数据渲染

虚拟列表是处理大数据量渲染的核心技术,只渲染可视区域元素,大幅提升性能。

问题背景

传统渲染问题

JavaScript
// ❌ 全量渲染万级数据
function renderList(data) {
  const container = document.getElementById('list');
  data.forEach(item => {
    const element = document.createElement('div');
    element.textContent = item.name;
    container.appendChild(element); // 10000个DOM节点
  });
}

// 问题:
// 1. DOM节点过多,内存占用大
// 2. 首次渲染时间长
// 3. 滚动卡顿
// 4. 重排重绘开销大

虚拟列表原理

JavaScript
只渲染可视区域内的元素
+ 维持滚动条真实高度(总数据高度)
+ 滚动时动态更新渲染内容

虚拟列表实现

基础结构

JavaScript
class VirtualList {
  constructor(options) {
    this.container = options.container;
    this.data = options.data;
    this.itemHeight = options.itemHeight; // 固定高度
    this.bufferSize = options.bufferSize || 5; // 缓冲区大小

    this.init();
  }

  init() {
    // 创建容器结构
    this.wrapper = document.createElement('div');
    this.wrapper.style.position = 'relative';
    this.wrapper.style.height = `${this.data.length * this.itemHeight}px`;

    this.content = document.createElement('div');
    this.content.style.position = 'absolute';
    this.content.style.top = '0';
    this.content.style.width = '100%';

    this.container.style.overflow = 'auto';
    this.container.appendChild(this.wrapper);
    this.wrapper.appendChild(this.content);

    // 监听滚动
    this.container.addEventListener('scroll', this.handleScroll.bind(this));

    // 首次渲染
    this.renderVisibleItems();
  }

  handleScroll() {
    this.renderVisibleItems();
  }

  getVisibleRange() {
    const scrollTop = this.container.scrollTop;
    const containerHeight = this.container.clientHeight;

    // 计算可视区域索引范围
    const startIndex = Math.floor(scrollTop / this.itemHeight);
    const endIndex = Math.ceil((scrollTop + containerHeight) / this.itemHeight);

    // 添加缓冲区
    const bufferedStart = Math.max(0, startIndex - this.bufferSize);
    const bufferedEnd = Math.min(this.data.length, endIndex + this.bufferSize);

    return { start: bufferedStart, end: bufferedEnd };
  }

  renderVisibleItems() {
    const { start, end } = this.getVisibleRange();

    // 清空当前内容
    this.content.innerHTML = '';

    // 渲染可视区域元素
    for (let i = start; i < end; i++) {
      const item = this.data[i];
      const element = this.createItemElement(item, i);
      this.content.appendChild(element);
    }

    // 定位内容区域
    this.content.style.top = `${start * this.itemHeight}px`;
  }

  createItemElement(item, index) {
    const element = document.createElement('div');
    element.style.height = `${this.itemHeight}px`;
    element.textContent = `${index}: ${item.name}`;
    return element;
  }
}

动态高度虚拟列表

JavaScript
class DynamicVirtualList {
  constructor(options) {
    this.container = options.container;
    this.data = options.data;
    this.estimatedHeight = options.estimatedHeight; // 预估高度

    this.heightCache = new Map(); // 实际高度缓存
    this.positionCache = []; // 位置缓存

    this.init();
  }

  init() {
    // 初始化位置缓存
    this.initPositionCache();

    // 创建结构
    this.wrapper = document.createElement('div');
    this.wrapper.style.position = 'relative';
    this.wrapper.style.height = `${this.getTotalHeight()}px`;

    this.content = document.createElement('div');
    this.wrapper.appendChild(this.content);

    this.container.style.overflow = 'auto';
    this.container.appendChild(this.wrapper);

    this.container.addEventListener('scroll', this.handleScroll.bind(this));
    this.renderVisibleItems();
  }

  initPositionCache() {
    this.positionCache = this.data.map((_, index) => ({
      index,
      height: this.estimatedHeight,
      top: index * this.estimatedHeight,
      bottom: (index + 1) * this.estimatedHeight
    }));
  }

  getTotalHeight() {
    if (this.positionCache.length === 0) return 0;
    return this.positionCache[this.positionCache.length - 1].bottom;
  }

  handleScroll() {
    this.renderVisibleItems();
  }

  getVisibleRange() {
    const scrollTop = this.container.scrollTop;
    const containerHeight = this.container.clientHeight;

    // 二分查找起始位置
    const start = this.binarySearchStart(scrollTop);
    const end = this.binarySearchEnd(scrollTop + containerHeight);

    return { start, end };
  }

  binarySearchStart(scrollTop) {
    let low = 0;
    let high = this.positionCache.length - 1;

    while (low < high) {
      const mid = Math.floor((low + high) / 2);
      const midBottom = this.positionCache[mid].bottom;

      if (midBottom <= scrollTop) {
        low = mid + 1;
      } else {
        high = mid;
      }
    }

    return low;
  }

  binarySearchEnd(scrollBottom) {
    let low = 0;
    let high = this.positionCache.length - 1;

    while (low < high) {
      const mid = Math.floor((low + high) / 2);
      const midTop = this.positionCache[mid].top;

      if (midTop < scrollBottom) {
        low = mid + 1;
      } else {
        high = mid;
      }
    }

    return low;
  }

  renderVisibleItems() {
    const { start, end } = this.getVisibleRange();

    this.content.innerHTML = '';

    for (let i = start; i < Math.min(end + 5, this.data.length); i++) {
      const item = this.data[i];
      const position = this.positionCache[i];

      const element = this.createItemElement(item, i);
      element.style.position = 'absolute';
      element.style.top = `${position.top}px`;

      // 渲染后获取实际高度
      this.content.appendChild(element);
      this.updateHeight(i, element.offsetHeight);
    }
  }

  updateHeight(index, actualHeight) {
    const cached = this.positionCache[index];

    if (cached.height !== actualHeight) {
      const diff = actualHeight - cached.height;

      // 更新当前位置
      cached.height = actualHeight;
      cached.bottom = cached.top + actualHeight;

      // 更新后续位置
      for (let i = index + 1; i < this.positionCache.length; i++) {
        this.positionCache[i].top += diff;
        this.positionCache[i].bottom += diff;
      }

      // 更新总高度
      this.wrapper.style.height = `${this.getTotalHeight()}px`;
    }
  }

  createItemElement(item, index) {
    const element = document.createElement('div');
    element.textContent = `${index}: ${item.name}`;
    element.style.width = '100%';
    return element;
  }
}

IntersectionObserver实现

JavaScript
class ObserverVirtualList {
  constructor(options) {
    this.container = options.container;
    this.data = options.data;
    this.itemHeight = options.itemHeight;

    this.sentinelTop = document.createElement('div');
    this.sentinelBottom = document.createElement('div');

    this.init();
  }

  init() {
    // 创建哨兵元素
    this.sentinelTop.style.height = '1px';
    this.sentinelBottom.style.height = '1px';

    this.container.appendChild(this.sentinelTop);

    // 初始渲染部分元素
    this.renderInitialItems();

    this.container.appendChild(this.sentinelBottom);

    // 设置观察器
    this.observer = new IntersectionObserver(this.handleIntersection.bind(this), {
      root: this.container,
      rootMargin: '100px'
    });

    this.observer.observe(this.sentinelTop);
    this.observer.observe(this.sentinelBottom);
  }

  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.target === this.sentinelTop && entry.isIntersecting) {
        this.prependItems();
      }
      if (entry.target === this.sentinelBottom && entry.isIntersecting) {
        this.appendItems();
      }
    });
  }

  prependItems() {
    // 向前加载更多
    if (this.startIndex > 0) {
      this.startIndex = Math.max(0, this.startIndex - 10);
      this.updateItems();
    }
  }

  appendItems() {
    // 向后加载更多
    if (this.endIndex < this.data.length) {
      this.endIndex = Math.min(this.data.length, this.endIndex + 10);
      this.updateItems();
    }
  }

  updateItems() {
    // 更新渲染内容
  }
}

React虚拟列表

react-window使用

JavaScript
import { FixedSizeList, VariableSizeList } from 'react-window';

// 固定高度列表
function FixedList({ data }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={data.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {data[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

// 动态高度列表
function VariableList({ data }) {
  const getItemSize = index => {
    return data[index].height || 50;
  };

  return (
    <VariableSizeList
      height={600}
      itemCount={data.length}
      itemSize={getItemSize}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {data[index].content}
        </div>
      )}
    </VariableSizeList>
  );
}

自定义React虚拟列表

vue
function VirtualList({ data, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0);

  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
  const visibleData = data.slice(startIndex, endIndex + 5);

  const handleScroll = (e) => {
    setScrollTop(e.currentTarget.scrollTop);
  };

  return (
    <div
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: data.length * itemHeight, position: 'relative' }}>
        {visibleData.map((item, i) => (
          <div
            key={startIndex + i}
            style={{
              position: 'absolute',
              top: (startIndex + i) * itemHeight,
              height: itemHeight
            }}
          >
            {item.name}
          </div>
        ))}
      </div>
    </div>
  );
}

Vue虚拟列表

vue-virtual-scroller

JavaScript
<template>
  <RecycleScroller
    :items="data"
    :item-size="50"
    :buffer="200"
    class="scroller"
  >
    <template #default="{ item, index }">
      <div class="item">
        {{ index }}: {{ item.name }}
      </div>
    </template>
  </RecycleScroller>
</template>

<script>
import { RecycleScroller } from 'vue-virtual-scroller';

export default {
  components: { RecycleScroller },
  props: ['data']
};
</script>

性能优化要点

销毁不可见元素

JavaScript
// ✅ 只保留可视区域DOM
// 滚动时移除离开可视区的元素,添加新进入的元素

// ❌ 保留所有已渲染元素
// 隐藏而非移除会导致DOM累积

减少渲染复杂度

JavaScript
// 简化列表项组件
function SimpleItem({ item }) {
  return (
    <div className="item">
      <span>{item.name}</span>
      <span>{item.value}</span>
    </div>
  );
}

// ❌ 复杂组件导致滚动卡顿
function ComplexItem({ item }) {
  return (
    <div>
      <HeavyComponent />
      <ExpensiveCalculation />
      <NestedComponents />
    </div>
  );
}

滚动事件优化

text
// 使用passive事件监听
container.addEventListener('scroll', handleScroll, { passive: true });

// 节流滚动处理
const throttledScroll = throttle(handleScroll, 16); // 约60fps
container.addEventListener('scroll', throttledScroll);

要点总结

  1. 核心原理:只渲染可视区域 + 占位空间维持滚动条
  2. 固定高度:简单计算索引范围,性能最优
  3. 动态高度:高度缓存 + 二分查找定位
  4. 缓冲区:前后预渲染防止滚动白屏
  5. IntersectionObserver:监听哨兵元素实现无限滚动
  6. 成熟库:react-window、vue-virtual-scroller开箱即用

存放路径:articles/JS/专家/高级性能分析/虚拟列表与大数据渲染.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 性能监控与 profiling 工具
下一篇 → 请求合并与缓存策略
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库