前端滚动加载与分页方案

www.jswusn.com Other 2026-09-07 15:30:12 1次浏览

前端滚动加载与分页方案

1. 问题描述

列表数据量大时,一次性渲染全部 DOM 会导致页面卡顿、首屏加载缓慢。常见的需求是滚动到底部加载更多分页下拉刷新等。但实现时很容易遇到:滚动监听触发不准确、重复请求、大量 DOM 造成性能下降、上拉下拉手势与原生滚动冲突。

典型的疑难场景如下:

// 一次性渲染 10 万条数据,页面直接卡死
const list = Array.from({ length: 100000 }, (_, i) => i)
document.body.innerHTML = list.map((i) => `<div>${i}</div>`).join('')

难点在于:

• 需要准确的"触底"检测并防重复请求
• 需要 懒加载/虚拟滚动 控制 DOM 数量;
• 移动端需要上拉加载 + 下拉刷新
• 需要处理加载状态、空态、错误态。


2. 方案一:滚动监听加载更多

原理:监听 scroll 事件,判断 scrollTop + clientHeight >= scrollHeight - threshold 即触发加载。需用防抖/节流控制触发频率,并用 loading 标志防止重复请求。

const listEl = document.getElementById('list')
let page = 1
let loading = false

async function loadMore() {
  if (loading) return
  loading = true
  try {
    const res = await fetch(`/api/items?page=${page}`)
    const items = await res.json()
    items.forEach((it) => {
      const div = document.createElement('div')
      div.textContent = it.name
      listEl.appendChild(div)
    })
    page++
  } finally {
    loading = false
  }
}

// 节流后的滚动监听
let ticking = false
window.addEventListener('scroll', () => {
  if (ticking) return
  ticking = true
  requestAnimationFrame(() => {
    const { scrollTop, clientHeight, scrollHeight } = document.documentElement
    if (scrollTop + clientHeight >= scrollHeight - 100) {
      loadMore()
    }
    ticking = false
  })
})

loadMore()

优点:实现简单、兼容性好、可控性强。
缺点scroll 高频触发需节流、触底判断易受布局影响、移动端触摸滚动兼容需留意。

3. 方案二:IntersectionObserver 懒加载分页

原理:使用 IntersectionObserver 观察一个"哨兵元素"(sentinel),当它进入视口时触发加载。相比 scroll 监听,它由浏览器原生调度、性能更好、判断更准确。

<div id="list"></div>
<!-- 哨兵元素:滚动到底部附近时进入视口 -->
<div id="sentinel" style="height: 1px;"></div>
const listEl = document.getElementById('list')
const sentinel = document.getElementById('sentinel')
let page = 1
let loading = false

const observer = new IntersectionObserver(async (entries) => {
  const entry = entries[0]
  // 哨兵进入视口 且 不在加载中 才加载
  if (entry.isIntersecting && !loading) {
    loading = true
    const res = await fetch(`/api/items?page=${page}`)
    const items = await res.json()
    items.forEach((it) => {
      const div = document.createElement('div')
      div.textContent = it.name
      listEl.appendChild(div)
    })
    page++
    loading = false
  }
}, {
  root: null,        // 视口
  threshold: 0,      // 刚出现即触发
  rootMargin: '100px' // 提前 100px 预加载
})

observer.observe(sentinel)


优点:性能好、准确、无需手动计算滚动位置、浏览器原生 API。
缺点:旧浏览器需 polyfill;回调默认在空闲时触发,时序需注意。

4. 方案三:虚拟滚动渲染大列表

原理:虚拟滚动只渲染可视区域内的行,通过绝对定位与 transform 控制偏移,动态增删 DOM。即使有几十万条数据,DOM 数量也保持固定,极大提升性能。

// 简易虚拟滚动
const container = document.getElementById('scroller')
const rowHeight = 40
const total = 100000
let startIndex = 0
const visibleCount = Math.ceil(container.clientHeight / rowHeight) + 5

function render(scrollTop) {
  startIndex = Math.max(0, Math.floor(scrollTop / rowHeight))
  const visible = []
  for (let i = startIndex; i < startIndex + visibleCount; i++) {
    if (i >= total) break
    visible.push(`<div style="position:absolute;transform: translateY(${i * rowHeight}px;height:${rowHeight}px;">第 ${i} 项</div>`)
  }
  container.innerHTML = visible.join('')
  // 撑开整体高度,让滚动条正确
  container.querySelector('.spacer') ||
    (container.style.height = total * rowHeight + 'px')
}

// 用内容定位:外层容器高度 + 内部 absolute 定位
container.addEventListener('scroll', (e) => {
  render(e.target.scrollTop)
})
render(0)
// 生产中推荐使用成熟库,如 react-window / vue-virtual-scroller
// npm i react-window
// import { FixedSizeList } from 'react-window'


优点:DOM 数量恒定、性能极佳、适合超大列表。
缺点:实现较复杂(固定/动态行高、滚动定位)、滑动体验需精细调优、无法用浏览器默认选中/查找。

5. 方案四:上拉加载与下拉刷新

原理:在移动端或触屏场景,通过 touchstart/touchmove/touchend 监听手势:下拉超过阈值触发刷新上拉触底触发加载。需处理手势位移、回弹动画与原生滚动冲突。

// 下拉刷新示例
const pullEl = document.getElementById('pull-indicator')
let startY = 0
let pulling = false

document.addEventListener('touchstart', (e) => {
  if (window.scrollY === 0) {
    startY = e.touches[0].clientY
    pulling = true
  }
})

document.addEventListener('touchmove', (e) => {
  if (!pulling) return
  const dist = e.touches[0].clientY - startY
  if (dist > 0) {
    pullEl.style.transform = `translateY(${dist}px)`
    pullEl.textContent = dist > 60 ? '松开刷新' : '下拉刷新'
  }
})

document.addEventListener('touchend', async (e) => {
  if (!pulling) return
  pulling = false
  const dist = e.changedTouches[0].clientY - startY
  if (dist > 60) {
    pullEl.textContent = '刷新中...'
    await refreshData() // 拉取第一页数据
    pullEl.style.transform = 'translateY(0)'
    pullEl.textContent = '下拉刷新'
  } else {
    pullEl.style.transform = 'translateY(0)'
  }
})

优点:移动端体验自然、符合交互习惯、可同时支持加载与刷新。
缺点:手势逻辑较复杂、需处理边界与动画、与嵌套滚动/浏览器下拉冲突需调优。

6. 方案对比表

方案
原理
优点
缺点
适用场景
滚动监听
scroll 事件 + 触底判断
简单、兼容好
需节流、触底判断易出错
通用分页、Web 端
IntersectionObserver
哨兵进入视口触发
性能好、准确
旧浏览器需 polyfill
无限滚动、懒加载
虚拟滚动
只渲染可见行
性能极佳、超大列表
实现复杂
超大列表(日志/长表格)
上拉下拉
手势监听
移动端体验好
手势逻辑复杂
移动端 H5、小程序

总结Web 端普通分页IntersectionObserver 更优雅;超大列表务必使用虚拟滚动移动端实现上拉加载 + 下拉刷新;滚动监听可作为兼容旧环境的兜底。生产项目中常组合:IntersectionObserver 分页 + 虚拟滚动渲染 + 状态管理(loading/empty/error)


上一篇:没有了!

Other

下一篇:UniApp开发中通用的Request请求封装request.js

技术分享

苏南名片

  • 联系人:技术-小吴
  • 电话:181-1554-0762
  • 邮箱:message@jswusn.com
  • 地址:江苏省苏州市相城区

热门文章

Copyright © 2018-2026 jswusn.com 版权所有

技术支持:苏州网站建设  苏ICP备18036849号