name: 无限滚动模式 description: 专家级框架,用于实现无限滚动,包括使用Intersection Observer、虚拟滚动、React Query和针对大数据集的性能优化。
无限滚动模式
概览
无限滚动是一种显示大数据集的技术,通过在用户滚动到预定义阈值时加载额外内容,而不是一次性加载所有数据。这项技能涵盖了Intersection Observer API用于检测视口进入、虚拟滚动以仅渲染可见项、React Query中的无限查询、滚动位置恢复和性能优化模式。
为什么这很重要
- 减少初始加载时间:最初只加载必要的数据,提高首次内容显示时间
- 提高用户参与度:连续滚动增加参与时间和内容发现
- 减少带宽使用:只加载可见项,减少不必要的带宽消耗
- 提高转化率:连续内容显示可以提高15-25%的转化率
- 增强移动性能:虚拟滚动提高硬件有限的移动设备的性能
核心概念
1. Intersection Observer
检测视口进入以实现懒加载:
- 哨兵元素:在列表底部放置不可见元素
- 阈值配置:控制何时触发加载(0-1.0)
- 根边距:在到达底部前加载(例如,“100px”)
- 观察者管理:正确连接/断开以防止内存泄漏
- 多个观察者:支持多个可滚动容器
2. 虚拟滚动
仅为性能渲染可见项:
- 窗口计算:根据滚动位置确定可见范围
- 过扫描:为平滑滚动在视口上方/下方渲染额外项
- 固定高度:通过已知项高度进行优化
- 动态高度:通过测量支持可变高度项
- 定位:使用top/transform进行绝对定位以提高滚动性能
3. 无限查询
React Query中的分页数据模式:
- 无限查询钩子:使用
useInfiniteQuery进行基于游标的分页 - 游标管理:处理来自API响应的nextCursor
- 页面合并:将页面展平为单个数组
- 重新获取:在窗口聚焦或手动刷新时重新获取所有页面
- 变异:在变异后更新缓存而不重新获取
4. 滚动位置恢复
在导航时记住滚动位置:
- 存储策略:使用sessionStorage保存滚动状态
- 键管理:每个页面/路由唯一键
- 在挂载时恢复:组件挂载时恢复位置
- 在滚动时保存:在滚动事件中保存位置
- 防抖:节流保存操作以避免过多写入
5. 性能优化
平滑滚动的技术:
- 图片懒加载:仅当进入视口时加载图片
- 防抖:节流滚动处理程序以防止过多渲染
- 记忆化:记忆化组件和回调
- 请求管理:在卸载时取消挂起的请求
- 内存管理:清理观察者和事件监听器
快速开始
- 设置Intersection Observer钩子:创建带有哨兵引用和加载回调的钩子
- 创建无限滚动组件:管理状态(项目、加载中、还有更多、错误)
- 实现加载更多逻辑:获取数据,追加到状态,处理完成
- 添加加载指示器:在加载期间显示旋转器
- 处理错误:显示错误消息并提供重试选项
- 实现虚拟滚动:对于大型列表(>1000项)
- 添加图片懒加载:仅当可见时加载图片
- 实现滚动恢复:保存/恢复滚动位置
- 优化性能:记忆化、节流,并正确清理
- 在移动设备上测试:确保在移动设备上性能流畅
// Intersection Observer 钩子
'use client'
import { useEffect, useRef, useCallback } from 'react'
interface UseInfiniteScrollOptions {
onLoadMore: () => void | Promise<void>
hasMore: boolean
isLoading: boolean
threshold?: number
rootMargin?: string
}
export function useInfiniteScroll({
onLoadMore,
hasMore,
isLoading,
threshold = 1.0,
rootMargin = '100px',
}: UseInfiniteScrollOptions) {
const sentinelRef = useRef<HTMLDivElement>(null)
const handleIntersection = useCallback(
(entries: IntersectionObserverEntry[]) => {
const [entry] = entries
if (entry.isIntersecting && hasMore && !isLoading) {
onLoadMore()
}
},
[hasMore, isLoading, onLoadMore]
)
useEffect(() => {
const sentinel = sentinelRef.current
if (!sentinel) return
const observer = new IntersectionObserver(handleIntersection, {
threshold,
rootMargin,
})
observer.observe(sentinel)
return () => observer.disconnect()
}, [handleIntersection, threshold, rootMargin])
return { sentinelRef }
}
// 无限滚动组件
function InfiniteScrollList({
fetchItems,
pageSize = 20,
}: {
fetchItems: (page: number) => Promise<Item[]>
pageSize?: number
}) {
const [items, setItems] = useState<Item[]>([])
const [page, setPage] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [error, setError] = useState<string | null>(null)
const loadMore = async () => {
if (isLoading || !hasMore) return
setIsLoading(true)
setError(null)
try {
const newItems = await fetchItems(page)
if (newItems.length === 0 || newItems.length < pageSize) {
setHasMore(false)
}
setItems((prev) => [...prev, ...newItems])
setPage((prev) => prev + 1)
} catch (err) {
setError('Failed to load items')
console.error('Load more failed:', err)
} finally {
setIsLoading(false)
}
}
const { sentinelRef } = useInfiniteScroll({
onLoadMore: loadMore,
hasMore,
isLoading,
})
useEffect(() => {
loadMore()
}, [])
return (
<div className="infinite-scroll-list">
<div className="items-grid">
{items.map((item) => (
<div key={item.id} className="item-card">
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
))}
</div>
<div ref={sentinelRef} className="sentinel" />
{isLoading && (
<div className="loading-indicator">
<div className="spinner" />
<p>Loading...</p>
</div>
)}
{!hasMore && !isLoading && (
<div className="end-message">
<p>No more items</p>
</div>
)}
{error && (
<div className="error-message">
<p>{error}</p>
<button onClick={loadMore}>Retry</button>
</div>
)}
</div>
)
}
生产清单
- [ ] 带有适当清理的Intersection Observer钩子
- [ ] 正确定位的哨兵元素
- [ ] 用户反馈的加载指示器
- [ ] 带重试选项的错误处理
- [ ] 列表末尾指示器
- [ ] 对于>1000项的列表实现虚拟滚动
- [ ] 实现图片懒加载
- [ ] 节流滚动处理程序
- [ ] 滚动位置恢复
- [ ] 防止内存泄漏(卸载时清理)
- [ ] 监控性能指标(FPS,内存)
- [ ] 在移动设备上测试
- [ ] 支持辅助功能(屏幕阅读器公告)
- [ ] 键盘导航支持
- [ ] API请求的速率限制
反模式
- 内存泄漏:组件卸载时未清理观察者
- 重复请求:在获取前未检查加载状态
- 性能差:对于大型列表未使用虚拟滚动
- 丢失滚动位置:在导航时未恢复滚动位置
- 无错误处理:未优雅处理失败请求
- 辅助功能问题:未向屏幕阅读器宣布新内容
- 过度获取:一次加载太多项目
- 阻塞UI:加载指示器阻止用户交互
- 跳过清理:未从事件/观察者中取消订阅
- 硬编码值:未使阈值、pageSize可配置
集成点
- React Query:
@tanstack/react-query用于无限查询模式 - 虚拟滚动:
react-window或react-virtualized用于大型列表 - React:核心React钩子用于状态管理
- TypeScript:数据结构的类型安全性
- 测试:Jest,React Testing Library用于组件测试
- 性能:Lighthouse,Chrome DevTools用于性能分析
- 分析:跟踪滚动深度,参与度指标
animation用于平滑加载动画react-best-practices用于React模式