虚拟列表实战:解决万级数据渲染卡顿难题

更新日期: 2025-11-03 阅读: 92 标签: 列表

我在开发一个数据展示页面时,需要显示上万条记录。结果页面直接卡死,滚动时就像拖着沉重的沙袋,用户体验极差。这个问题困扰了我很久,直到我找到了虚拟列表这个解决方案。


什么是虚拟列表?

想象一下你去图书馆借书。如果管理员把十万本书全部摊在大厅里,你肯定无从下手。但实际的情况是,管理员只把热门书籍放在展示区,其他书籍都整齐存放在书架上。

虚拟列表就是采用类似的思路。它不会一次性渲染所有数据,而是只显示用户当前能看到的部分。其他数据就像存放在书架上,需要时再拿出来展示。

传统的数据渲染方式是这样的:

// 直接渲染10000条数据,dom元素过多导致页面卡顿
{items.map(item => (
  <div key={item.id} className="item">
    {item.content}
  </div>
))}

使用虚拟列表后:

// 只渲染可见区域的10-20条数据
{visibleItems.map(item => (
  <div key={item.id} className="item">
    {item.content}
  </div>
))}


虚拟列表的核心原理

虚拟列表的实现基于三个关键计算:可见区域起始位置、结束位置和元素定位。

1. 基础参数计算

首先需要确定几个基本参数:

const containerHeight = 600;  // 容器可视高度
const itemHeight = 50;        // 每个列表项的高度
const totalCount = 10000;     // 总数据量

2. 滚动位置计算

当用户滚动列表时,需要计算当前显示的是第几条数据:

// 计算起始索引
const startIndex = Math.floor(scrollTop / itemHeight);

// 计算结束索引
const visibleCount = Math.ceil(containerHeight / itemHeight);
const endIndex = Math.min(startIndex + visibleCount + 5, totalCount);

这里多渲染5个元素是为了防止滚动时出现空白区域。

3. 元素定位

每个可见元素都需要精确定位:

const getItemStyle = (index) => ({
  position: 'absolute',
  top: index * itemHeight,
  width: '100%',
  height: itemHeight
});


完整实现代码

下面是一个可直接使用的虚拟列表组件:

import react, { useState, useRef, useCallback } from 'react';

const VirtualList = ({ 
  items, 
  itemHeight = 50, 
  containerHeight = 400,
  renderItem 
}) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);

  // 计算可见区域
  const startIndex = Math.floor(scrollTop / itemHeight);
  const visibleCount = Math.ceil(containerHeight / itemHeight);
  const endIndex = Math.min(
    startIndex + visibleCount + 5, // 多渲染5个避免空白
    items.length
  );

  // 获取可见项
  const visibleItems = items.slice(startIndex, endIndex);

  // 处理滚动
  const handleScroll = useCallback(() => {
    if (containerRef.current) {
      setScrollTop(containerRef.current.scrollTop);
    }
  }, []);

  // 容器总高度
  const totalHeight = items.length * itemHeight;

  return (
    <div
      ref={containerRef}
      style={{
        height: containerHeight,
        overflow: 'auto',
        position: 'relative',
        border: '1px solid #ddd'
      }}
      onScroll={handleScroll}
    >
      {/* 占位元素,保证滚动条正确 */}
      <div style={{ height: totalHeight }} />
      
      {/* 渲染可见项 */}
      {visibleItems.map((item, index) => (
        <div
          key={item.id}
          style={{
            position: 'absolute',
            top: (startIndex + index) * itemHeight,
            width: '100%',
            height: itemHeight,
            display: 'flex',
            alignItems: 'center',
            padding: '0 16px',
            borderBottom: '1px solid #f0f0f0',
            backgroundColor: 'white'
          }}
        >
          {renderItem ? renderItem(item) : item.content}
        </div>
      ))}
    </div>
  );
};

// 使用示例
const DemoPage = () => {
  // 生成测试数据
  const mockData = Array.from({ length: 10000 }, (_, index) => ({
    id: `item-${index}`,
    content: `列表项 ${index + 1}`,
    description: `这是第 ${index + 1} 条数据的详细描述`
  }));

  const renderItem = (item) => (
    <div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
      <span>{item.content}</span>
      <span style={{ color: '#666', fontSize: '12px' }}>{item.description}</span>
    </div>
  );

  return (
    <div style={{ padding: '20px' }}>
      <h2>虚拟列表演示 - 10000条数据</h2>
      <VirtualList
        items={mockData}
        itemHeight={60}
        containerHeight={500}
        renderItem={renderItem}
      />
    </div>
  );
};

export default DemoPage;


解决常见问题

1. 滚动闪烁问题

快速滚动时可能出现空白区域,可以通过预渲染解决:

// 增加缓冲项数量
const buffer = 8;
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
const endIndex = Math.min(
  startIndex + visibleCount + buffer * 2,
  items.length
);

2. 动态高度支持

如果列表项高度不固定,需要更复杂的计算:

// 使用数组记录每个项的位置
const [positions, setPositions] = useState([]);

// 更新项位置信息
const updatePosition = (index, height) => {
  setPositions(prev => {
    const newPositions = [...prev];
    newPositions[index] = {
      height,
      top: index === 0 ? 0 : newPositions[index - 1].top + newPositions[index - 1].height
    };
    return newPositions;
  });
};

3. 性能优化建议

// 使用防抖避免频繁渲染
const handleScroll = useCallback(() => {
  requestAnimationFrame(() => {
    if (containerRef.current) {
      setScrollTop(containerRef.current.scrollTop);
    }
  });
}, []);

// 使用React.memo避免不必要的重渲染
const MemoizedItem = React.memo(({ item, style }) => (
  <div style={style}>
    {item.content}
  </div>
));


性能对比实测

通过实际测试,可以看到明显的性能差异:

传统渲染方式:

  • DOM元素数量:10000个

  • 内存占用:约45-60MB

  • 滚动帧率:5-10 FPS

  • 页面加载时间:3-5秒

虚拟列表方案:

  • DOM元素数量:15-25个

  • 内存占用:约5-10MB

  • 滚动帧率:55-60 FPS

  • 页面加载时间:0.5-1秒


适用场景

虚拟列表特别适合以下场景:

  1. 数据表格:显示大量行数据

  2. 聊天记录:展示历史消息

  3. 社交动态:时间线内容

  4. 商品列表:电商平台商品展示

  5. 日志查看器:系统日志浏览


进阶技巧

1. 无限滚动加载

结合虚拟列表实现无限滚动:

const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(false);

const handleScroll = useCallback(() => {
  const { scrollTop, clientHeight, scrollHeight } = containerRef.current;
  
  // 接近底部时加载更多
  if (scrollHeight - scrollTop - clientHeight < 100 && !loading) {
    setLoading(true);
    loadMoreData().then(newData => {
      setData(prev => [...prev, ...newData]);
      setLoading(false);
    });
  }
}, [loading]);

2. 搜索筛选支持

虚拟列表也可以很好地支持搜索功能:

const [filteredItems, setFilteredItems] = useState(items);

const handleSearch = (keyword) => {
  const result = items.filter(item => 
    item.content.toLowerCase().includes(keyword.toLowerCase())
  );
  setFilteredItems(result);
};


总结

虚拟列表通过只渲染可见区域内容,大幅提升了大数据量场景下的页面性能。实现的核心在于精确计算可见区域和正确的位置定位。

相比传统渲染方式,虚拟列表能够:

  • 减少90%以上的DOM元素数量

  • 提升页面加载速度

  • 保证滚动的流畅性

  • 降低内存占用

虽然实现起来比直接渲染复杂一些,但对于需要处理大量数据的现代Web应用来说,这种投入是非常值得的。掌握虚拟列表技术,能够让你在面临性能挑战时多一个有效的解决方案。

本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!

链接: https://fly63.com/article/detial/13127

CSS实现无外边框列表效果

使用外层容器切割:给每一个 li 设定右边框和下边框线,使用CSS选择器此方法仅适用于每行固定显示两个li的情况,不需要计算宽高,也不需要设置父容器。使用table通过CSS选择器li:nth-last-child(2)和li:last-child隐藏最后两个li的下边框

html列表简单介绍

无序列表使用标签:<ul>,<li>属性:disc,circle,square;有序列表使用标签:<ol>,<li>属性:A,a,I,i,start;嵌套列表使用标签:<ul>,<ol>,<li> ;自定义列表使用标签:<dl>,<dt>,<dd>

css样式li不显示点点什么原因?怎么解决?

实际上用了overflow:hidden 会影响 list-style,即当ul 中的li 的overflow 为hidden的时候, list-style不起作用,不显示前面的点、圈等样式。

React列表中实现文案多行收起展开的功能

在我们平时的业务开发中经常会用到文案超出只有收起,点击在展示全部文案;通常的使用时使用css来实现

Vue.js 多选列表(Multi-Select)组件

多选列表 (Multi-Select) 是一种将所有选项列出,并允许用户利用 Ctrl/Shift 键进行多选的 UI 元素。这是一种常见的设计元素。有时候为了节省空间,我们会将选项折叠于 Combo Box 中

如何让10万条数据的小程序列表如丝般顺滑

某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:

了解虚拟列表背后原理,轻松实现虚拟列表

比如 umy-ui (ux-table)虚拟列表table组件, vue-virtual-scroller 以及 react-virtualized 这些优秀的插件快速满足业务需要。为了理解插件背后的原理机制,我们实现一个自己简易版的虚拟列表,希望在实际业务项目中能带来一些思考和帮助。

React 中的列表渲染为什么要加 key

常用写法是用 Arrary.prototype.map 方法,将数组形式的数据映射为 JSX.Element 数组,并嵌入到组件要返回的 JSX.Element 中,如下:

浅说虚拟列表的实现原理

在正文之前,先对虚拟列表做个简单的定义。根据上文,虚拟列表是按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!