虚拟滚动仅控制可视区域渲染,万级数据需滚动监听+节流+分页/游标接口配合;应提前预加载(如距底200px),用sentinel或scrollTop计算页码/offset;后端须服务端分页,推荐游标分页;前端需缓存、防抖、AbortController取消请求,并与react-window等库集成。

虚拟滚动本身不负责数据拉取,它只控制“当前可视区域渲染哪些项”。万级数据分批次加载,关键在于把滚动位置映射成页码或偏移量,再按需请求后端接口。核心是前端滚动监听 + 节流 + 分页/游标式接口配合。
滚动位置转为数据请求参数
不能等用户滚到底部才加载下一页(体验差、易漏数据),而应提前预加载。常见做法是监听 scroll 或使用 IntersectionObserver 监控“临近可视区底部”的占位元素(如 sentinel 元素):
- 设定一个“提前加载距离”,比如距离底部 200px 就触发下一页请求
- 根据当前
scrollTop + clientHeight和容器总高度估算当前“逻辑页码”或offset - 若用分页(page + size),则
page = Math.floor(visibleStartIndex / pageSize) + 1;若用游标(如 lastId),则记录上一批末尾 ID 作为下次起点
后端接口需支持精准分段查询
后端不能返回全量数据再前端切片,必须由服务端完成分页裁剪。推荐两种模式:
-
偏移量分页(Offset-based):适合数据量稳定、排序字段唯一且无频繁增删的场景。接口如
GET /api/items?offset=1000&limit=50。注意 MySQL 的OFFSET在万级偏移时性能下降明显,需配合覆盖索引或改用游标 -
游标分页(Cursor-based):更高效可靠。前端传上一批最后一条的排序字段值(如
last_created_at或last_id),后端查WHERE created_at > ? ORDER BY created_at LIMIT 50。避免跳过问题,也规避了 OFFSET 性能陷阱
前端状态管理与缓存策略
避免重复请求、白屏、错乱,需维护清晰的数据状态:
- 用 Map 或数组缓存已加载的“数据块”,key 可为
page或cursor;未命中时显示骨架屏或 loading 占位 - 滚动中快速来回时,可能触发多次请求,需加防抖/节流(如 150ms 内只发一次),并取消前序未完成的请求(用
AbortController) - 保持“逻辑列表长度”准确(如通过接口返回 total 或估算),用于计算滚动条高度和位置映射;total 不必实时精确,可初始加载时获取一次,后续增量更新
与虚拟滚动库(如 react-window、vue-virtual-scroller)集成要点
以 react-window 为例,它只渲染可见行,但需要你提供 itemCount 和 itemData:
-
itemCount设为预估总数(如后端返回的 total),或设为 Infinity + 滚动到底部自动加载(需配合 onItemsRendered 回调) -
itemData应是合并后的数据源:已加载的数据块拼接 + 未加载位置用 placeholder 对象(如{ loading: true }),组件内据此决定渲染内容或骨架 - 监听
onItemsRendered,当渲染范围靠近末尾(overscanStopIndex >= itemCount - 5),触发下一批加载















