直接使用 IntersectionObserver 而非 scroll 事件,因其性能更优、不受 transform/overflow 影响、天然适配响应式布局;需配合占位哨兵、正确清理、合理设置 threshold 与骨架样式,并处理断点切换和后台标签页等边界场景。

为什么直接用 IntersectionObserver 而不是监听 scroll 事件
监听 window.onscroll 在滚动频繁时会触发大量回调,尤其在移动端容易卡顿或丢帧;IntersectionObserver 是浏览器原生的轻量级 API,只在目标元素进入/离开视口时触发,性能更可控,且天然支持响应式布局下的动态高度变化。
关键点:IntersectionObserver 不依赖元素绝对位置计算,也不受 transform 或 overflow 容器影响(只要父容器没设 contain: strict),这对 Tailwind 中常用 flex、grid 或 max-h-[60vh] 的列表容器很友好。
- 务必给观察目标加一个占位元素(如空
<div class="h-px">)放在列表末尾,避免“滚动到底但不触发加载”的边界问题 <li>不要在 <code>observer.observe()前把目标元素display: none或从 DOM 移除,否则观察会失效 - 响应式场景下,如果列表项高度随屏幕宽度变化(比如从单列变双列),需调用
observer.disconnect(); observer.observe(target)重置观察,否则可能漏触发 - 骨架项必须和真实列表项使用相同的
flex/grid结构,否则响应式断点切换时高度错乱 - 避免对整个列表容器加
opacity-75,这会让已加载项也变灰;应只对骨架项本身设透明度或背景色 - 加载中提示(如 “Loading more…”)建议用
text-center py-4 text-sm text-gray-500,并加col-span-full(Grid)或w-full(Flex),确保横跨所有列
如何用 Tailwind 控制加载状态与骨架屏样式
无限滚动中用户感知的关键是“正在加载”是否自然。Tailwind 本身不提供 loading 状态逻辑,但能快速构建符合当前设计系统的视觉反馈。
推荐做法:用 animate-pulse + bg-gray-200 组合做骨架,配合 min-h-16 / h-4 等固定尺寸类保证骨架高度稳定(避免内容加载后页面跳动)。
立即学习“前端免费学习笔记(深入)”;
为什么 useEffect 里初始化 IntersectionObserver 要加 cleanup
React 组件卸载时若不清理 IntersectionObserver,会导致内存泄漏——观察器仍持有对 DOM 元素的引用,且回调可能在组件已销毁后执行,引发 Cannot update a component while rendering 或 Cannot perform a React state update on an unmounted component 错误。
正确写法是在 useEffect 返回函数中调用 observer.unobserve(target) 和 observer.disconnect():
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => { if (entry.isIntersecting) loadMore() },
{ threshold: 0.1 }
)
if (sentinelRef.current) observer.observe(sentinelRef.current)
return () => {
if (sentinelRef.current) observer.unobserve(sentinelRef.current)
observer.disconnect()
}
}, [loadMore])
-
threshold: 0.1比0更可靠:元素哪怕只有 10% 进入视口就触发,避免快速滚动时错过 - 不要把
observer存到useState或useRef里再反复读取——每次useEffect执行都应创建新实例 - 如果列表支持搜索/过滤导致数据重置,需在重置时手动调用
observer.disconnect()并重建,否则旧观察器还在监听已被移除的哨兵节点
响应式断点下如何避免重复加载或漏加载
Tailwind 的 md:flex-row、lg:grid-cols-2 会改变列表项渲染高度,进而影响 IntersectionObserver 的触发时机。常见现象是:桌面端加载了 20 条,切到手机端后只显示前 10 条,但哨兵元素被挤到可视区外,不再触发后续加载。
- 解决方案:监听
resize事件,在断点变化后主动调用observer.observe(sentinelRef.current)(注意防抖,避免高频触发) - 更稳妥的做法是把哨兵元素放在列表最底部且不参与布局(用
absolute bottom-0 w-full h-px+ 父容器relative),这样它始终“贴底”,不受 flex/grid 排列影响 - 服务端分页参数(如
page、limit)必须和客户端渲染逻辑解耦:按“已渲染条数 / 每页条数”算当前页,而不是依赖 UI 层的列数变化
真正麻烦的不是代码写几行,而是当用户快速拖拽滚动条、又突然切到后台标签页、再切回来时,isIntersecting 可能为 false 却没触发回调——这时得靠定时 fallback 或手动触发一次 observer.takeRecords() 检查。这个细节多数教程会跳过,但它决定了上线后 QA 能不能复现偶发的“卡在最后一页不动”问题。


















