虚拟滚动 + 加载:让万级列表丝般顺滑

面对10万行数据的表格需求:页面卡顿、滚动卡顿、内存暴涨...真是让人抓狂!

你是否曾见过这样的灾难场景:用户打开一个长列表页面,浏览器直接卡死崩溃?这就像邀请1000个人同时进电梯------DOM元素过多会导致浏览器处理能力崩溃。虚拟滚动技术就是解决这一痛点的方案。

为什么传统列表会"死亡"?

先看看传统列表处理大数据的问题所在:

html 复制代码
<!-- 灾难代码:直接渲染10万条数据 -->
<ul>
  <li v-for="item in 100000">...</li>
</ul>

致命问题

  • ⚡️ 内存爆炸:每个DOM节点占用约1-2KB,10万节点 = 200MB内存!
  • 🐌 渲染阻塞:首次渲染耗时可能超过10秒
  • 🔥 滚动卡顿:每次滚动都需要重新布局绘制整个列表

虚拟滚动:化腐朽为神奇的魔法

核心原理:只渲染你看得见的部分

虚拟滚动像高级魔术师,它的把戏很简单:

  1. 视觉欺骗:整个容器高度设为总高度(撑起滚动条)
  2. 动态窗口:只渲染可视区域内的少量元素(通常20-50个)
  3. 位移魔法:使用transform平移内容制造流畅滚动假象

与传统分页的对比

方案 用户体验 内存占用 滚动流畅度 SEO友好
传统分页 需要点击
无限滚动 连续浏览
虚拟滚动 无缝体验 极低 极高

手撸一个虚拟滚动加载器

基础骨架HTML

html 复制代码
<div class="container">
  <div class="viewport" id="viewport">
    <div class="list-phantom"></div> <!-- 撑起总高度的影子元素 -->
    <div class="list-content"></div> <!-- 实际渲染区 -->
  </div>
</div>

关键CSS布局

css 复制代码
.viewport {
  height: 500px;
  overflow-y: scroll;
  position: relative;
  border: 1px solid #eee;
}

.list-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1; /* 隐藏影子 */
}

.list-content {
  position: absolute;
  left: 0;
  right: 0;
  top: 0; 
}

.list-item {
  padding: 12px;
  border-bottom: 1px solid #f0f0f0;
  box-sizing: border-box;
}

JS实现虚拟滚动(含加载更多)

javascript 复制代码
class VirtualScroller {
  constructor({
    viewport,
    total = 0,          // 总数据量
    itemHeight = 50,    // 每项高度(可扩展为动态高度)
    renderCount = 20,   // 渲染数量
    loadThreshold = 200 // 加载阈值
  }) {
    this.viewport = viewport;
    this.total = total;
    this.itemHeight = itemHeight;
    this.renderCount = renderCount;
    this.loadThreshold = loadThreshold;
    
    this.data = [];       // 当前展示数据
    this.startIndex = 0;  // 起始索引
    this.endIndex = 0;    // 结束索引
    this.offsetY = 0;     // Y轴位移
    
    this.init();
  }

  init() {
    // 设置影子元素高度(撑起滚动条)
    const phantom = this.viewport.querySelector('.list-phantom');
    phantom.style.height = `${this.total * this.itemHeight}px`;
    
    // 绑定滚动事件
    this.viewport.addEventListener('scroll', this.handleScroll.bind(this));
    
    // 初始渲染
    this.render();
  }

  // 核心渲染逻辑
  render() {
    const viewport = this.viewport;
    const scrollTop = viewport.scrollTop;
    
    // 计算当前应展示的区间索引
    this.startIndex = Math.floor(scrollTop / this.itemHeight);
    this.endIndex = Math.min(
      this.startIndex + this.renderCount,
      this.total - 1
    );
    
    // 计算位移(关键!)
    this.offsetY = this.startIndex * this.itemHeight;
    
    const content = viewport.querySelector('.list-content');
    content.style.transform = `translateY(${this.offsetY}px)`;
    
    // 渲染可见项
    this.renderItems();
    
    // 检查是否需要加载更多
    this.checkLoadMore();
  }
  
  renderItems() {
    const content = this.viewport.querySelector('.list-content');
    let fragment = document.createDocumentFragment();
    
    // 创建新的项
    for (let i = this.startIndex; i <= this.endIndex; i++) {
      const item = document.createElement('div');
      item.className = 'list-item';
      item.style.height = `${this.itemHeight}px`;
      item.dataset.index = i;
      
      // 替换为实际的渲染逻辑
      item.innerHTML = `数据项 #${i} | 随机ID: ${Math.random().toString(36).substr(2, 9)}`;
      
      fragment.appendChild(item);
    }
    
    // 高效批量更新
    content.innerHTML = '';
    content.appendChild(fragment);
  }
  
  // 检查是否需要加载更多数据
  checkLoadMore() {
    const scrollHeight = this.viewport.scrollHeight;
    const scrollTop = this.viewport.scrollTop;
    const clientHeight = this.viewport.clientHeight;
    
    // 到达加载临界点
    if (scrollHeight - scrollTop - clientHeight < this.loadThreshold) {
      this.loadMoreData();
    }
  }
  
  // 模拟数据加载
  loadMoreData() {
    if (this.total >= 1000) return; // 防止无限加载
    
    this.total += 50; // 每次加载50条
    const phantom = this.viewport.querySelector('.list-phantom');
    phantom.style.height = `${this.total * this.itemHeight}px`;
    
    console.log(`加载更多数据,当前总数: ${this.total}`);
  }

  handleScroll() {
    // 使用节流优化性能
    if (this.timer) clearTimeout(this.timer);
    this.timer = setTimeout(() => this.render(), 20);
  }
}

// 初始化虚拟滚动
const viewport = document.getElementById('viewport');
new VirtualScroller({
  viewport,
  total: 100,           // 初始100条
  itemHeight: 50,       // 预估高度
  loadThreshold: 200    // 距离底部200px加载
});

高级优化技巧

动态高度处理

上面的示例假设所有项目高度相同,现实情况更复杂:

javascript 复制代码
// 创建高度映射表
this.heightMap = new Map();

// 渲染时获取实际高度并记录
const observe = new ResizeObserver(entries => {
  entries.forEach(entry => {
    const index = entry.target.dataset.index;
    this.heightMap.set(index, entry.contentRect.height);
  });
});

// 在创建item后添加监听
observe.observe(item);

滚动节流与防抖优化

javascript 复制代码
handleScroll() {
  // RAF优化滚动性能
  if (!this.raf) {
    this.raf = requestAnimationFrame(() => {
      this.render();
      this.raf = null;
    });
  }
}

滚动位置保持

在动态加载数据时,保持用户当前查看的位置:

javascript 复制代码
loadMoreData() {
  const scrollTop = this.viewport.scrollTop;
  
  // 增加更多数据...
  
  // 恢复滚动位置
  this.viewport.scrollTop = scrollTop;
}

现成方案推荐(开箱即用)

  1. Vue生态系统

  2. React解决方案

  3. 原生JS工具

    • titanium - 超轻量级方案(仅3KB)
    • virtua - 最新高性能实现

应用场景实例

  1. 电商平台

    • 商品列表(20000+ SKU)
    • 订单历史(用户多年数据)
    js 复制代码
    // 电商场景特殊处理
    registerImageLazyLoad() {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            const img = entry.target.querySelector('img');
            img.src = img.dataset.src;
            observer.unobserve(entry.target);
          }
        });
      });
    }
  2. 实时监控系统

    • 日志流(持续增长的数据)
    • 设备状态面板
  3. 社交应用

    • 聊天记录(支持快速跳转)
    • 动态信息流

避坑

  1. 快速滚动空白问题

    • 增加缓冲区:额外渲染屏幕外20%的内容
    js 复制代码
    this.startIndex = Math.max(
      0, 
      Math.floor(scrollTop / itemHeight) - extraItems
    );
  2. 滚动条跳动

    • 使用精确高度映射代替估算值
    • 初始化时计算平均高度
  3. 内存泄漏

    • 清除卸载的ResizeObserver
    • 使用弱引用(Map vs WeakMap)
javascript 复制代码
// 使用弱引用避免内存泄漏
const heightRegistry = new WeakMap();

小结

虚拟滚动+加载技术的核心思想:用计算换性能,用时间换空间。通过本文的实现,您已经掌握了:

  1. 虚拟滚动核心原理:视窗裁剪 + 动态位移
  2. 滚动加载关键点:触底检测 + 数据增量
  3. 生产级优化:动态高度 + 性能调优

当您下次遇到上万条数据的需求时,不再需要说服产品减少条目数------虚拟滚动让浏览器轻松处理百万级数据!

相关推荐
koooo~27 分钟前
JavaScript中的Window对象
开发语言·javascript·ecmascript
hbrown37 分钟前
Flask+LayUI开发手记(十一):选项集合的数据库扩展类
前端·数据库·python·layui
猫头虎41 分钟前
什么是 npm、Yarn、pnpm? 有什么区别? 分别适应什么场景?
前端·python·scrapy·arcgis·npm·beautifulsoup·pip
迷曳1 小时前
27、鸿蒙Harmony Next开发:ArkTS并发(Promise和async/await和多线程并发TaskPool和Worker的使用)
前端·华为·多线程·harmonyos
安心不心安2 小时前
React hooks——useReducer
前端·javascript·react.js
像风一样自由20202 小时前
原生前端JavaScript/CSS与现代框架(Vue、React)的联系与区别(详细版)
前端·javascript·css
啃火龙果的兔子2 小时前
react19+nextjs+antd切换主题颜色
前端·javascript·react.js
_pengliang2 小时前
小程序按住说话
开发语言·javascript·小程序
布兰妮甜2 小时前
创建游戏或互动体验:从概念到实现的完整指南
javascript·游戏开发·游戏ai·互动体验·用户输入处理
paid槮2 小时前
HTML5如何创建容器
前端·html·html5