无限滚动核心是按需加载,通过IntersectionObserver监听占位元素进入视口触发分页请求,需防重复加载、节点回收、游标分页对齐及观察器清理。

无限滚动的核心实现逻辑
无限滚动本质是监听滚动位置,当用户快到底部时触发数据加载。关键不在“无限”,而在“按需加载”——页面不预加载全部数据,只维护当前可视区域附近的数据。
最简可行方案用 window.addEventListener('scroll') + IntersectionObserver 两种方式,后者更推荐:它不依赖频繁的 scroll 事件回调,浏览器原生优化了性能。
-
IntersectionObserver观察一个占位元素是否进入视口 - 触发回调后调用接口拉取下一页,追加到列表末尾
- 加载中状态要阻断重复触发(比如设置
loading = true标志位) - 接口返回空数组或
hasMore: false时,应停止监听或移除观察器
为什么 scroll 事件容易引发卡顿?
直接监听 window.onscroll 或 addEventListener('scroll') 会在滚动过程中高频触发(每秒几十次),若回调里做 DOM 操作、计算 offset、或未节流,立刻导致主线程阻塞。
典型错误写法:
立即学习“Java免费学习笔记(深入)”;
window.addEventListener('scroll', () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
loadMore(); // 每次都执行,没防抖也没节流
}
});正确做法必须加节流(throttle)或改用 IntersectionObserver。即使节流,仍需注意:滚动期间反复读取 document.body.offsetHeight 或 scrollY 会强制触发回流(layout),尤其在长列表中代价很高。
DOM 节点爆炸与内存泄漏风险
无限滚动不清理旧节点,列表越滚越长,DOM 树体积膨胀,最终导致渲染变慢、内存占用飙升、甚至页面崩溃。
- 真实项目中,建议限制 DOM 节点数(如最多保留当前页 ±3 页的内容),超出部分用虚拟滚动(virtualized list)替代
- 不要用
innerHTML += ...追加内容——它会重绘整个容器,应使用document.createElement+appendChild或DocumentFragment - 卸载组件时(如 React useEffect cleanup / Vue onUnmounted),务必调用
observer.disconnect(),否则观察器持续驻留内存 - 避免在每个 item 中绑定独立事件监听器;用事件委托(如给列表容器绑定
click,再用e.target.dataset.id区分)
服务端与前端分页对齐的关键细节
前端“无限滚动”常被误认为和后端“分页”天然匹配,其实极易出错。
- 后端接口必须支持基于游标(cursor)或时间戳(timestamp)的分页,而非仅
page=2&size=20—— 否则插入新数据会导致漏项或重复 - 前端需保存上一次请求的
cursor或最后一条的id,作为下一页参数,不能简单递增page值 - 加载失败时,应保留已加载内容,并提供手动重试入口(如底部显示“加载失败,点击重试”),而不是清空列表
- 首次加载完成前,建议展示骨架屏(skeleton)而非空白,避免用户误判为无数据
真正难的不是“怎么滚下去”,而是“怎么滚得稳、不崩、不丢、不卡”。游标分页、节点回收、观察器清理——这三处漏掉任何一环,上线后都会变成深夜报警电话里的关键词。










