InfiniteScrollPatterns InfiniteScrollPatterns

专家级框架,用于实现无限滚动,包括使用Intersection Observer、虚拟滚动、React Query和针对大数据集的性能优化。

前端开发 0 次安装 0 次浏览 更新于 3/5/2026

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. 性能优化

平滑滚动的技术:

  • 图片懒加载:仅当进入视口时加载图片
  • 防抖:节流滚动处理程序以防止过多渲染
  • 记忆化:记忆化组件和回调
  • 请求管理:在卸载时取消挂起的请求
  • 内存管理:清理观察者和事件监听器

快速开始

  1. 设置Intersection Observer钩子:创建带有哨兵引用和加载回调的钩子
  2. 创建无限滚动组件:管理状态(项目、加载中、还有更多、错误)
  3. 实现加载更多逻辑:获取数据,追加到状态,处理完成
  4. 添加加载指示器:在加载期间显示旋转器
  5. 处理错误:显示错误消息并提供重试选项
  6. 实现虚拟滚动:对于大型列表(>1000项)
  7. 添加图片懒加载:仅当可见时加载图片
  8. 实现滚动恢复:保存/恢复滚动位置
  9. 优化性能:记忆化、节流,并正确清理
  10. 在移动设备上测试:确保在移动设备上性能流畅
// 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请求的速率限制

反模式

  1. 内存泄漏:组件卸载时未清理观察者
  2. 重复请求:在获取前未检查加载状态
  3. 性能差:对于大型列表未使用虚拟滚动
  4. 丢失滚动位置:在导航时未恢复滚动位置
  5. 无错误处理:未优雅处理失败请求
  6. 辅助功能问题:未向屏幕阅读器宣布新内容
  7. 过度获取:一次加载太多项目
  8. 阻塞UI:加载指示器阻止用户交互
  9. 跳过清理:未从事件/观察者中取消订阅
  10. 硬编码值:未使阈值、pageSize可配置

集成点

  • React Query@tanstack/react-query用于无限查询模式
  • 虚拟滚动react-windowreact-virtualized用于大型列表
  • React:核心React钩子用于状态管理
  • TypeScript:数据结构的类型安全性
  • 测试:Jest,React Testing Library用于组件测试
  • 性能:Lighthouse,Chrome DevTools用于性能分析
  • 分析:跟踪滚动深度,参与度指标
  • animation用于平滑加载动画
  • react-best-practices用于React模式

进一步阅读