⚡ 一个高性能的虚拟滚动库,采用跳跃式按需渲染策略,专为处理海量数据设计。
- ⚡ 跳跃式渲染 - 可以从任意位置开始渲染,无需预先计算
- 🚀 零跳动 - 精心设计的占位符管理,滚动如丝般顺滑
- 🔄 双向扩展 - 向上或向下滚动时自动追加新元素
- 🎯 原生 JavaScript - 零依赖,仅 10 KB(gzip 后更小)
- 💪 海量数据 - 轻松处理 100万+ 条数据
- 🔧 丰富的 API - 完整的数据操作和滚动控制方法
- 📱 移动端友好 - 完美支持触摸滚动
FastScrollView 采用创新的跳跃式按需渲染策略,与传统虚拟滚动有本质区别:
- ❌ 需要预先计算所有元素的位置
- ❌ 初始化时需要遍历所有数据
- ❌ 跳转到 #5000 时,需要渲染 #0-#5000 所有元素
- ❌ 频繁调整 DOM 导致跳动
- ❌ 对动态高度支持不够友好
- ✅ 无需预先计算 - 不遍历所有元素,直接从目标位置开始
- ✅ 跳跃式跳转 - 跳转到 #5000 时,只渲染 #5000 附近 20-30 个元素
- ✅ 双向扩展 - 向上/向下滚动时自动追加,不删除已有元素
- ✅ 零跳动 - 不调整 scrollTop,完全依赖占位符
- ✅ 更快初始化 - 只渲染首屏,启动几乎瞬间完成
场景:10000 条数据,跳转到 #5000
┌─────────────────────────────────┐
│ topSpacer (250,000px) │ ← #0-#4999 未渲染(占位符)
├─────────────────────────────────┤
│ ┌───────────────────────────┐ │
│ │ Item #5000 (已渲染) │ │
│ │ Item #5001 (已渲染) │ │ ← 仅渲染可见区域
│ │ ... │ │ 约 20-30 个元素
│ │ Item #5020 (已渲染) │ │
│ └───────────────────────────┘ │
├─────────────────────────────────┤
│ bottomSpacer (249,000px) │ ← #5021-#9999 未渲染(占位符)
└─────────────────────────────────┘
结果:
✅ DOM 中只有 20 个元素
✅ 滚动条显示完整列表长度
✅ 可以继续向上或向下滚动
✅ 滚动时自动扩展渲染范围
| 操作 | 传统虚拟滚动 | FastScrollView |
|---|---|---|
| 初始化 10万数据 | ~500ms(计算位置) | ~10ms(渲染首屏) |
| 跳转到 #5000 | 渲染 5000+ 元素 | 渲染 20-30 元素 |
| DOM 节点数 | 5000+ | 20-30 |
| 滚动跳动 | 可能跳动 | 完全无跳动 ✅ |
npm install fast-scrollview<script src="https://unpkg.com/fast-scrollview/dist/fast-scrollview.min.js"></script># 克隆仓库
git clone git@github.com:cooolinx/fast-scrollview.js.git
cd fast-scrollview
# 安装依赖
npm install
# 构建
npm run build<div id="scroll-container" style="height: 600px;"></div>// 1. 准备数据(可以是任意大小的数组)
const items = [];
for (let i = 0; i < 100000; i++) {
items.push({
id: i,
title: `数据项 ${i}`,
content: `这是第 ${i} 条数据`
});
}
// 2. 定义渲染函数
function renderItem(item, index, totalSize) {
const div = document.createElement('div');
div.className = 'list-item';
div.innerHTML = `
<strong>#${index}</strong>
<span>${item.title}</span>
<p>${item.content}</p>
`;
return div;
}
// 3. 创建 FastScrollView 实例
const fsv = new FastScrollView(
'#scroll-container', // 容器元素或选择器
items, // 数据数组
renderItem, // 渲染函数
{
bufferThreshold: 2, // 缓冲阈值(可选,默认 2)
align: 'top', // 对齐方式(可选,'top' 或 'bottom',默认 'top')
onScroll: (info) => { // 滚动回调(可选)
console.log('滚动中...', info);
}
}
);
// 4. 使用跳跃式渲染
fsv.scrollToItem(50000); // 跳转到 #50000,只渲染附近元素!new FastScrollView(container, items, render, options)- container (HTMLElement | string) - 容器元素或 CSS 选择器
- items (Array) - 要渲染的数据数组
- render (Function) - 渲染函数,签名:
(item, index, totalSize) => HTMLElement | string - options (Object, 可选) - 配置选项
bufferThreshold(number) - 缓冲阈值,默认 2(表示提前2个屏幕高度触发渲染)align(string) - 对齐方式,'top'(默认)或'bottom'(底部对齐)onScroll(Function) - 滚动时的回调函数
设置新的数据数组,会清除所有高度缓存。
fsv.setItems(newItemsArray);更新指定索引的数据项。
fsv.setItem(10, { id: 10, title: '更新后的标题' });在指定位置插入数据项。
fsv.insertItem(5, { id: 999, title: '插入的数据' });在数组末尾添加数据项。
fsv.append({ id: 1001, title: '新数据' });在数组开头添加数据项。
fsv.prepend({ id: 1002, title: '最新数据' });删除数据项,可以传入数据项本身或索引。
// 通过索引删除
fsv.remove(5);
// 通过数据项删除
fsv.remove(item);跳转到指定的数据项(核心方法),支持跳跃式渲染。
// 通过索引跳转 - 只渲染该位置附近的元素
fsv.scrollToItem(5000); // 跳转到 #5000,只渲染 #5000 附近 20-30 个元素
// 通过数据项跳转
fsv.scrollToItem(item);特点:
- ✅ 跳跃式渲染:只渲染目标位置附近的元素
- ✅ 不会全量渲染:即使跳转到 #50000 也只渲染附近内容
- ✅ 智能优化:如果目标 item 已渲染,直接滚动不重新渲染(性能更优)
- ✅ 基于缓存计算:已访问过的位置使用精确的缓存高度
跳转到顶部。
fsv.scrollToTop(); // 等同于 fsv.scrollToItem(0)跳转到底部。
fsv.scrollToBottom(); // 等同于 fsv.scrollToItem(items.length - 1)注意: scrollToTop() 和 scrollToBottom() 也是按需渲染,不会加载整个列表。
获取当前可视区域的信息。
const range = fsv.getVisibleRange();
// 返回: { start: 10, end: 30, count: 20 }判断是否滚动到底部(用于聊天应用场景)。
const isAtBottom = fsv.isAtScrollBottom(); // 默认容差 10px
const isAtBottom2 = fsv.isAtScrollBottom(5); // 自定义容差 5px参数:
threshold(number, 可选) - 容差值(像素),默认为 10px
返回值:
boolean- 是否在底部
使用场景:
在聊天应用中,当用户正在查看历史消息时,新消息到来不应该自动滚动;只有当用户在底部时,才应该自动滚动到新消息。
// 智能自动滚动示例
function addNewMessage(message) {
// 添加消息前,先检查是否在底部
const wasAtBottom = chatView.isAtScrollBottom();
// 添加新消息
chatView.append(message);
// 只有之前在底部时,才自动滚动
if (wasAtBottom) {
chatView.scrollToBottom();
}
}刷新显示,重新测量所有元素的高度。
fsv.refresh();销毁实例,清理事件监听器和数据。
fsv.destroy();FastScrollView 支持 align: 'bottom' 参数,特别适合聊天应用等需要从底部显示内容的场景。
const chatView = new FastScrollView(
'#chat-container',
messages,
renderMessage,
{
align: 'bottom', // 👈 启用底部对齐
bufferThreshold: 2
}
);- ✅ 自动贴底 - 当消息数量少时,自动贴在底部显示(像真实的聊天应用)
- ✅ 智能定位 -
setItems()后自动滚动到最底部 - ✅ 完美体验 - 无论内容多少,始终从底部开始显示
// 默认模式 (align: 'top')
// 1-2 条消息时,从顶部开始显示
┌─────────────────┐
│ 消息1 │
│ 消息2 │
│ │
│ (空白) │
│ │
└─────────────────┘
// 底部对齐模式 (align: 'bottom')
// 1-2 条消息时,贴在底部显示
┌─────────────────┐
│ │
│ (空白) │
│ │
│ 消息1 │
│ 消息2 │
└─────────────────┘- 💬 聊天应用
- 📱 消息列表
- 💭 评论区
- 📝 日志查看器
项目包含多个完整的示例,演示不同的使用场景:
展示 10万条数据的基本用法,包括所有 API 方法的演示。
- ✅ 10万条数据流畅滚动
- ✅ 完整的操作按钮演示
- ✅ 实时性能监控
展示不同高度的卡片元素,验证动态高度测量功能。
- ✅ 2万条不同高度的卡片
- ✅ 自动高度测量和缓存
- ✅ 准确的滚动定位
模拟真实的聊天应用场景。
- ✅ 5万条聊天消息
- ✅ 实时发送消息
- ✅ 完整的用户交互
测试 align: 'bottom' 参数的各种场景。
- ✅ 测试 1-2 条消息贴底显示
- ✅ 测试多条消息滚动到底部
- ✅ 与默认 top 对齐模式对比
演示 isAtScrollBottom() 方法的实际应用。
- ✅ 智能判断是否需要自动滚动
- ✅ 对比智能滚动和强制滚动的体验差异
- ✅ 实时消息流演示
测试优化后的 scrollToItem() 方法性能。
- ✅ 演示已渲染 item 的直接滚动(无需重新渲染)
- ✅ 实时性能指标显示
- ✅ 对比跳跃渲染和直接滚动的性能差异
# 构建项目
npm run build
# 使用浏览器打开
open examples/index.html或者使用本地服务器:
npx http-server -p 8080
# 然后访问 http://localhost:8080/examples/FastScrollView 适用于以下场景:
- 📋 长列表 - 电商商品列表、搜索结果等
- 💬 聊天应用 - 历史消息记录、群聊记录
- 📊 数据表格 - 大量数据的表格展示
- 📱 社交媒体 - 时间线、动态列表
- 📁 文件管理器 - 大量文件/文件夹列表
- 📝 日志查看器 - 系统日志、应用日志
- 初始化 - 从 scrollTop=0 开始,只渲染首屏内容(约 20-30 个元素)
- 向下滚动 - 检测滚动到接近已渲染区域底部时,自动追加新元素
- 向上滚动 - 检测滚动到接近已渲染区域顶部时,自动在前面插入元素
- 跳跃跳转 - 清空现有渲染,从目标位置重新开始渲染
- 占位符管理 - topSpacer 和 bottomSpacer 代表未渲染区域的高度
// 向下扩展:当滚动到接近底部时
if (scrollBottom + threshold > renderedBottom) {
appendItems(...); // 追加新元素
}
// 向上扩展:当滚动到接近顶部时
if (scrollTop - threshold < renderedTop) {
prependItems(...); // 在前面插入元素
}| 数据量 | 传统渲染 | FastScrollView | DOM 节点比 |
|---|---|---|---|
| 1,000 | 1,000 个 | ~20 个 | 50x ⬇️ |
| 10,000 | 10,000 个 | ~20 个 | 500x ⬇️ |
| 100,000 | 100,000 个 | ~20 个 | 5000x ⬇️ |
设置合适的预估高度可以提升位置计算的准确性(特别是在跳转时):
const fsv = new FastScrollView(container, items, render, {
estimatedItemHeight: 100 // 设置为元素的平均高度
});建议:
- 统一高度的列表:设置为实际高度
- 动态高度的列表:设置为平均高度
- 高度差异大的列表:设置为中位数高度
控制何时触发新元素的渲染:
const fsv = new FastScrollView(container, items, render, {
bufferThreshold: 3 // 提前 3 个屏幕高度触发渲染(默认 2)
});说明:
- 值越大,渲染的元素越多,滚动越流畅,但内存占用越高
- 值越小,渲染的元素越少,内存占用越低,但快速滚动可能出现空白
- 默认值 2 是平衡性能和体验的最佳值
const fsv = new FastScrollView(container, items, render, {
onScroll: (info) => {
console.log('可视范围:', info.visibleStart, '-', info.visibleEnd);
console.log('滚动位置:', info.scrollTop);
// 无限滚动加载
if (info.visibleEnd >= items.length - 10) {
loadMoreData();
}
}
});this.renderedStartIndex // 已渲染区域的起始索引
this.renderedEndIndex // 已渲染区域的结束索引this.renderedCache.set(index, { height }) // 缓存已测量的高度topSpacer.height = sum(heights[0 ~ renderedStartIndex])
bottomSpacer.height = sum(heights[renderedEndIndex ~ items.length])// 场景:10000 条数据
// 初始化
fsv = new FastScrollView(...)
// → 渲染 #0-#20
// → renderedStartIndex = 0, renderedEndIndex = 20
// 跳转到 #5000
fsv.scrollToItem(5000)
// → 清空现有渲染
// → 渲染 #5000-#5020
// → renderedStartIndex = 5000, renderedEndIndex = 5020
// → topSpacer = 5000 * 50px = 250,000px
// → bottomSpacer = 4980 * 50px = 249,000px
// 向下滚动
// → 自动追加 #5021-#5040
// → renderedEndIndex = 5040
// → bottomSpacer 减少
// 向上滚动
// → 自动在前面插入 #4980-#4999
// → renderedStartIndex = 4980
// → topSpacer 减少- 不调整 scrollTop - 绝不在用户滚动时修改滚动位置
- 只扩展不删除 - 已渲染的元素保留在 DOM 中
- 占位符稳定 - 只在扩展渲染范围时更新占位符
- 测量后不补偿 - 新元素测量完高度后,不调整位置
原因: 元素高度变化但没有重新测量。
解决: 调用 refresh() 方法重新测量所有高度。
fsv.refresh();原因: 使用估算高度计算位置,未测量过的元素用 estimatedItemHeight。
解决:
- 设置更准确的预估高度
- 第二次跳转到同一位置会更准确(因为已有缓存)
const fsv = new FastScrollView(container, items, render, {
estimatedItemHeight: 80 // 调整为实际平均高度
});原因: 缓冲阈值太小,来不及渲染新元素。
解决: 增加缓冲阈值。
const fsv = new FastScrollView(container, items, render, {
bufferThreshold: 3 // 提前 3 个屏幕高度触发渲染
});原因: 内容变化导致高度变化。
解决: 使用 setItem() 方法,会自动清除缓存并更新。
fsv.setItem(index, newItem); // 自动清除该项的高度缓存// ✅ 推荐:使用 scrollToItem 跳转
fsv.scrollToItem(5000); // 只渲染附近元素
// ❌ 不推荐:遍历到目标位置
for (let i = 0; i < 5000; i++) {
// 会渲染所有中间元素
}// ✅ 推荐:使用批量方法
fsv.beginUpdate();
items.forEach(item => fsv.items.push(item));
fsv.endUpdate();
// ❌ 不推荐:逐个操作
items.forEach(item => fsv.append(item)); // 每次都会重新渲染// ✅ 准确的预估高度
const avgHeight = items.reduce((sum, item) => sum + measureHeight(item), 0) / items.length;
const fsv = new FastScrollView(container, items, render, {
estimatedItemHeight: avgHeight
});
// ⚠️ 随意设置可能导致跳转位置偏差// 长期运行的应用,定期清理缓存
setInterval(() => {
if (fsv.renderedCache.size > 1000) {
fsv.refresh(); // 清理缓存,重新开始
}
}, 60000);// 窗口大小变化时刷新
window.addEventListener('resize', () => {
fsv.refresh();
});- ✅ Chrome (最新版)
- ✅ Firefox (最新版)
- ✅ Safari (最新版)
- ✅ Edge (最新版)
- ✅ iOS Safari
- ✅ Android Chrome
最低要求: 支持 ES6 的现代浏览器
MIT License
欢迎提交 Issue 和 Pull Request!
如有问题或建议,请提交 Issue 或联系作者。
Happy Coding! 🎉