无限滚动瀑布流实战:懒加载与性能优化的完美结合

186 阅读5分钟

旅游项目中的图片展示如何做到丝滑流畅?关键在于懒加载与无限滚动的默契配合!

今天在开发旅游类项目的瀑布流页面时,我实现了一套高性能的图片展示方案。这个方案核心在于两点:无限滚动加载图片懒加载,配合React Hooks和CSS模块化,让用户体验达到极致。可以看下面的具体效果

6.gif

下面我来详细拆解实现过程。

无限滚动加载:滚动即加载的魔法

核心文件:Waterfall.jsxCollection.jsx

无限滚动的基本原理是:当用户滚动到页面底部时,自动触发加载更多数据的操作。我们使用IntersectionObserver API来实现这个效果:

// Waterfall.jsx 关键代码
useEffect(() => {
  const observer = new IntersectionObserver(([entry], obs) => {
    if (entry.isIntersecting) {
      fetchMore() // 触发加载更多
    }
  })
  if (loader.current) observer.observe(loader.current)
  return () => observer.disconnect()
}, [])

这里的关键元素是位于瀑布流底部的加载指示器:

<div ref={loader} className={styles.loader}>加载中</div>

当这个元素进入视口时,观察者会触发fetchMore函数请求更多数据。配合父组件Collection.jsx的使用:

// Collection.jsx
const Collection = () => {
  const { loading, images, fetchMore } = useImageStore()
  
  useEffect(() => {
    fetchMore() // 初始化加载
  }, [])
  
  return <Waterfall images={images} fetchMore={fetchMore} loading={loading} />
}

这种设计实现了加载与展示分离:瀑布流组件只负责展示和触发加载,实际的数据获取逻辑由父组件通过状态管理注入。

双列瀑布流布局:CSS的巧妙设计

核心文件:waterfall.module.css

要实现美观的瀑布流布局,CSS的编写至关重要:

.wrapper {
  display: flex;
  justify-content: space-between;
  padding: 16px;
  flex-wrap: nowrap;
  position: relative;
}

.column {
  width: 48%;
  margin: 0 1%;
  display: flex;
  flex-direction: column;
}

.loader {
  position: absolute;
  bottom: 0;
  right: 0;
  left: 0;
  height: 80px;
  text-align: center;
}

这里使用Flex布局创建两个等宽列(各占48%+1%边距)。特别值得注意的是加载器的定位:

  • position: absolute使其脱离文档流
  • bottom: 0定位在容器底部
  • left:0right:0保证全宽

在JSX中,图片被分配到两列中:

<div className={styles.column}>
  {images.filter((_, i) => i % 2 === 0).map(img => (
    <ImageCard key={img.id} {...img} />
  ))}
</div>
<div className={styles.column}>
  {images.filter((_, i) => i % 2 !== 0).map(img => (
    <ImageCard key={img.id} {...img} />
  ))}
</div>

通过简单的奇偶过滤,实现图片在双列中的均匀分布。

图片懒加载:性能优化的关键

核心文件:ImageCard.jsxcard.module.css

当页面包含大量图片时,直接加载所有图片会导致性能问题。解决方案是懒加载——只有当图片进入视口时才加载真实资源。

实现方案:

// ImageCard.jsx
useEffect(() => {
  const observer = new IntersectionObserver(([entry], obs) => {
    if (entry.isIntersecting) {
      const img = entry.target
      const oImg = document.createElement('img')
      oImg.src = img.dataset.src
      oImg.onload = () => {
        img.src = img.dataset.src // 图片加载完成后设置真实src
      }
      obs.unobserve(entry.target) // 加载后停止观察
    }
  })
  observer.observe(imgRef.current)
}, [])

在渲染部分,我们使用data-src存储真实URL:

<div style={{ height }} className={styles.card}>
  <img ref={imgRef} data-src={url} className={styles.img} />
</div>

配合精心设计的CSS保证图片展示效果:

.card {
  width: 100%;
  margin: 1%;
  background-color: #eee; /* 占位背景色 */
  overflow: hidden;
  border-radius: 16px;
}

.img {
  width: 100%;
  height: 100%;
  object-fit: cover; /* 关键属性:保持宽高比并裁剪 */
  display: block;
}

object-fit: cover是这里的神来之笔,它确保不同尺寸的图片都能完美填充容器,保持视觉一致性。

数据模拟:Mock.js的妙用

核心文件:data.js

在开发阶段,我们使用Mock.js模拟后端API:

// 图片数据生成
const getImages = (page, pageSize = 10) => {
  return Array.from({ length: pageSize }, (_, i) => ({
    id: `${page}-${i}`,
    height: Mock.Random.integer(300, 500),
    url: Mock.Random.image('300x400', Mock.Random.color(), '#fff', 'img')
  }))
}

// API路由
{
  url: '/api/images',
  method: 'get',
  response: ({ query }) => {
    const page = Number(query.page) || 1;
    return {
      code: 0,
      data: getImages(page)
    }
  }
}

Mock.js的Random.image()方法生成占位图片URL,格式为:
'https://dummyimage.com/300x400/ff0000/ffffff'

其中参数依次为:尺寸、背景色、文字颜色和文字内容。这种模拟方式让我们无需等待真实API就能开发前端功能。

数据获取:axios封装

核心文件:home.jsconfig.js

我们对axios进行了统一封装,简化API调用:

// config.js
axios.defaults.baseURL = 'http://localhost:5173/api'

// 响应拦截
axios.interceptors.response.use((data) => {
  return data.data // 直接返回核心数据
})

// home.js
export const getImages = (page) => {
  return axios.get('/images', { params: { page } })
}

这种封装带来两大好处:

  1. 统一管理基础URL
  2. 响应拦截器自动剥离外层结构,直接返回核心数据

性能优化技巧总结

  1. 观察器资源释放
    在useEffect清理函数中调用observer.disconnect(),防止内存泄漏

    return () => observer.disconnect()
    
  2. 图片加载优化
    使用临时Image对象预加载,避免阻塞渲染:

    const oImg = document.createElement('img')
    oImg.src = img.dataset.src
    
  3. 虚拟滚动替代方案
    虽然项目未实现,但思路值得提及:

    • 仅渲染可视区域内图片
    • 动态计算图片位置
    • 使用空白占位保持滚动条高度
  4. 请求防抖
    可在fetchMore函数中加入防抖逻辑,避免滚动事件频繁触发请求

遇到的坑与解决方案

  1. 图片闪烁问题
    初始:直接设置src导致加载过程中的空白
    解决:添加背景色占位 + 加载完成后再显示
  2. 重复加载
    初始:未取消观察导致进入视口多次触发
    解决:加载后调用obs.unobserve(entry.target)
  3. 图片尺寸跳跃
    现象:懒加载导致布局跳动
    解决:在卡片div上设置固定高度style={{ height }}

总结

今天的开发让我深刻理解了现代前端性能优化的核心思路:按需加载。通过Intersection Observer API,我们实现了:

  1. 滚动到页面底部自动加载(无限滚动)
  2. 图片进入视口才加载(懒加载)
  3. 组件化设计保证可维护性

技术栈组合:

  • React Hooks(useEffect, useRef)
  • CSS Modules样式隔离
  • Axios网络请求
  • Mock.js数据模拟

最终效果:用户无限滚动浏览图片,系统按需加载资源,完美平衡体验与性能。这种模式特别适合旅游类项目的图片展示,后续可扩展为三列布局、加入分类过滤等功能。