基于JavaScript的图片懒加载与无限滚动小项目实例

作者:安全边界时间:2026-10-30 18:35:05浏览:477

通过一个完整的原生JavaScript小项目实例,详细讲解如何使用现代IntersectionObserver API实现高性能的图片懒加载与无限滚动,包含完整代码、原理解析、步骤说明与常见问题排查。

在一个包含大量图片的电商产品流或社交媒体动态页中,若页面加载时一次性请求数十甚至上百张大图,极易导致主线程被网络下载与解码阻塞,产生首屏白屏、卡顿与流量浪费。解决这一体验问题的核心路径在于两项技术的组合应用:一是图片懒加载(Lazy Loading),使图片仅在即将进入屏幕可视区域时才发起实际网络加载;二是无限滚动(Infinite Scroll),在用户滚动接近页面底部时异步拉取下一批列表数据并动态追加到DOM中。本文通过一个精简、独立的原生JavaScript小项目,演示如何基于现代浏览器提供的 IntersectionObserver(交叉观察器)API,以轻量、零依赖的方式协同构建这两项功能。

基于JavaScript的图片懒加载与无限滚动小项目实例
图1:图片懒加载与无限滚动双观察器协作架构

一、项目核心机制与选型分析

传统的滚动监听通常在 window.addEventListener('scroll', handler) 中通过调用 getBoundingClientRect() 计算目标元素距顶部的距离。这种方式存在明显的性能缺陷:用户滚动时 scroll 事件会在短时间内连续数十次触发,频繁调用几何计算会导致浏览器强制同步重排(Reflow),在低端设备上易出现掉帧。

现代前端开发中推荐使用 IntersectionObserver。该接口脱离了主线程高频事件轮询,将目标元素与根容器视口的相交检测交给底层渲染引擎异步计算,仅在相交状态跨越预设阈值时才通过回调通知脚本。本项目需要建立两个观察器对象各司其职:

  • 图片观察器(imageObserver):观察所有带有 data-src 的图片元素。当图片接近视口(可设置提前量)时,将真实图片地址写入 src,完成加载后立即解除对其观察,释放资源。
  • 触底观察器(infiniteScrollObserver):观察列表底部的占位节点(通常称为“哨兵节点”或 #sentinel)。当哨兵元素暴露在视口中时,说明用户已滚动至底部,触发获取下一批数据逻辑。
基于JavaScript的图片懒加载与无限滚动小项目实例
图2:项目开发落地与执行各阶段步骤

二、基础骨架与样式准备

为了让实例可独立运行,我们需要先构建HTML文档骨架。核心容器包括两个部分:用于承载图片卡片的列表容器 #gallery,以及位于列表最末端的哨兵容器 #sentinel。

在样式设计上,未加载的图片必须设置固定宽高比或骨架背景占位。若图片没有宽高占位,所有图片的初始高度均为0,会导致大量尚未加载的卡片聚集在视口内,引起观察器误判并瞬间发起全部图片的下载请求,使懒加载彻底失效。

<!-- HTML 结构 -->
<div class="container">
  <div id="gallery" class="gallery-grid"></div>
  <!-- 触底哨兵元素 -->
  <div id="sentinel" class="loading-status">
    <span class="spinner"></span> 正在加载精彩图片...
  </div>
</div>

配套的基础CSS样式如下:

/* CSS 样式设计 */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 16px;
  padding: 16px 0;
}

.img-card {
  position: relative;
  background-color: #f1f5f9;
  border-radius: 8px;
  overflow: hidden;
  /* 关键:使用固定的高宽比占位,防止未加载时高度塌陷引发批量触发 */
  aspect-ratio: 4 / 3;
  display: flex;
  align-items: center;
  justify-content: center;
}

.lazy-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.35s ease-in-out;
}

/* 图片真实加载成功后追加淡入类 */
.lazy-img.loaded {
  opacity: 1;
}

.loading-status {
  text-align: center;
  padding: 24px;
  color: #64748b;
  font-size: 14px;
}

三、核心逻辑实现步骤

步骤一:实现图片懒加载观察器

创建 IntersectionObserver 实例并配置参数。关键参数 rootMargin: "200px 0px" 表示视口向外扩张200像素的虚拟警戒线。图片尚未完全进入屏幕、仍在视口下方200像素时,即提前开始网络下载,当用户滚动到该位置时图片往往已经加载完毕,从而消除空白闪烁感。

// 1. 创建图片懒加载观察器
const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    // 检查目标是否与视口相交
    if (entry.isIntersecting) {
      const img = entry.target;
      const realSrc = img.dataset.src;

      if (realSrc) {
        // 替换真实图片地址发起请求
        img.src = realSrc;
        img.addEventListener('load', () => {
          img.classList.add('loaded');
          img.removeAttribute('data-src');
        }, { once: true });
      }

      // 关键:已触发的图片必须立即取消观察,避免重复触发与内存占用
      observer.unobserve(img);
    }
  });
}, {
  root: null, // 默认以浏览器可视窗口作为视口
  rootMargin: '200px 0px', // 提前 200px 预加载
  threshold: 0.01 // 出现 1% 边缘即触发
});
基于JavaScript的图片懒加载与无限滚动小项目实例
图3:单个图片卡片的生命周期时序演进图

步骤二:实现触底无线滚动与数据拉取

无限滚动的关键在于请求锁(isLoading)与终点判定(hasMore)。当哨兵元素相交时,若当前处于请求中或已无更多数据,直接返回,避免在网络延迟期间因用户轻微晃动页面引发多次重复拉取。

// 2. 状态变量定义
let currentPage = 1;
const pageSize = 8;
const maxPages = 5; // 模拟总共5页数据
let isLoading = false;
let hasMore = true;

// 模拟异步数据请求
function fetchImages(page, limit) {
  return new Promise(resolve => {
    setTimeout(() => {
      // 构造模拟图片数据
      const list = [];
      for (let i = 1; i <= limit; i++) {
        const id = (page - 1) * limit + i;
        list.push({
          id,
          title: `图集作品 #${id}`,
          // 使用稳定可访问的图片生成服务(或项目中实际接口)
          url: `https://picsum.photos/id/${(id * 7) % 100 + 10}/600/450`
        });
      }
      resolve(list);
    }, 600); // 模拟600ms网络延迟
  });
}

// 渲染图片卡片到DOM并纳入懒加载观察
function renderGalleryItems(items) {
  const gallery = document.getElementById('gallery');
  const fragment = document.createDocumentFragment();

  items.forEach(item => {
    const card = document.createElement('div');
    card.className = 'img-card';

    const img = document.createElement('img');
    img.className = 'lazy-img';
    // 关键:初始时不设置 src,仅设置 data-src
    img.dataset.src = item.url;
    img.alt = item.title;

    card.appendChild(img);
    fragment.appendChild(card);

    // 将新创建的未加载图片节点挂入图片观察器
    imageObserver.observe(img);
  });

  gallery.appendChild(fragment);
}

步骤三:配置触底哨兵观察器

对 #sentinel 元素建立专门的观察器。当用户向下滚动,页面底部的哨兵元素暴露在视口中时,自动调用加载逻辑,并在拉取完毕后自增页码;若达到上限,则注销哨兵并展示“已加载全部内容”提示。

// 3. 触底哨兵观察器
const sentinel = document.getElementById('sentinel');

async function loadNextBatch() {
  if (isLoading || !hasMore) return;
  isLoading = true;

  try {
    const data = await fetchImages(currentPage, pageSize);
    renderGalleryItems(data);

    currentPage++;
    if (currentPage > maxPages) {
      hasMore = false;
      sentinel.innerHTML = '已加载全部精彩图片';
      scrollObserver.unobserve(sentinel); // 彻底注销触底观察器
    }
  } catch (err) {
    console.error('加载图片列表异常:', err);
    sentinel.innerHTML = '加载失败,请重试';
  } finally {
    isLoading = false;
  }
}

const scrollObserver = new IntersectionObserver((entries) => {
  const [entry] = entries;
  if (entry.isIntersecting) {
    loadNextBatch();
  }
}, {
  rootMargin: '100px 0px' // 距离底部100px时提前请求下一页
});

// 启动监听哨兵
scrollObserver.observe(sentinel);
提示:由于页面初次加载时 #gallery 内没有任何卡片,哨兵节点 #sentinel 必然位于视口内。因此,只要调用 scrollObserver.observe(sentinel),观察器便会立即触发第一次首屏数据拉取,无需在脚本末尾手动额外调用一次加载函数。

四、验证效果与常见陷阱排查

在浏览器中运行上述代码时,可通过以下步骤进行验证:

  1. 打开 Chrome DevTools,切换至 Network(网络) 面板,勾选 Img 过滤项。
  2. 刷新页面,观察首屏网络请求:只有第一批展示出来的数张图片发起了图片格式请求,其余后续数据仅获取了URL列表,并未触发图片下载。
  3. 缓慢向下滚动,当视口靠近底部卡片时,新的图片请求按需在网络面板中依次出现;继续触底后,哨兵元素触发下一批卡片渲染。
  4. 快速拉动滚动条测试防抖与状态锁,确认不会因连续触底而发送重复页码的重复请求。
注意避免以下三个开发陷阱:
  1. 忘记在触发后取消观察(unobserve):如果不及时解绑,当用户向上或向下再次滚动使该图片反复进出视口时,回调函数将反复执行无意义的逻辑。
  2. 未对占位容器设置最小高度或固定比例:若容器在未加载图片时高度塌陷为0,初次追加的数十张图片瞬间全部位于视口内部,导致所有图片同时触发请求,懒加载彻底形同虚设。
  3. 忽略触底状态并发锁:异步拉取接口需要数百毫秒网络耗时,如果不设置 isLoading 标识,在慢速网络或快速滑动时会导致哨兵反复相交,进而产生并发重复请求。
基于JavaScript的图片懒加载与无限滚动小项目实例
图4:核心机制设计与工程化优化要点总结

五、完整代码组织回顾

通过合理结合 IntersectionObserver 的两个独立实例,不仅可以实现关注点分离(图片加载与列表分页互不干扰),还能用不到100行原生代码构建出性能优异、结构清晰的瀑布流或图集加载组件。在实际生产项目中,开发者还可以根据实际业务需求,将数据源替换为真实的 RESTful 或 GraphQL 接口,并引入错误重试机制或更细腻的骨架动画,以进一步提升用户体验。

作者最新文章
相关文章
更多