图片懒加载原理与实战:从滚动监听到 Intersection Observer

0 阅读7分钟

图片懒加载原理与实战:从滚动监听到 Intersection Observer

在网页性能优化中,图片懒加载是一项基础且重要的技术。本文将带你从零了解懒加载的必要性、实现原理,并通过代码实战掌握现代浏览器提供的 Intersection Observer API,让你的页面加载速度更上一层楼。

一、为什么需要图片懒加载?

在传统的网页开发中,我们通常会在 <img> 标签的 src 属性中直接写入图片的 URL。当浏览器解析到该标签时,就会立即发起 HTTP 请求去下载图片。

如果一个页面包含大量图片,尤其是像电商、图库这类场景,所有图片同时请求会带来两个严重问题:

  1. 首屏加载缓慢:浏览器并发请求数量有限(通常 6-8 个),图片占用了大量请求队列,导致 HTML、CSS、核心 JS 等关键资源被阻塞,页面白屏时间变长。
  2. 浪费用户流量:用户可能根本没有滚动到页面底部,却已经加载了所有图片,对于移动端用户而言,这是一笔不必要的流量开销。

懒加载的核心思想:只加载当前视口(viewport)内的图片,当用户滚动到某个位置时,再去加载对应的图片。这是一种典型的“按需加载”策略,能够显著提升首屏性能,减少资源浪费。

二、传统懒加载方案:滚动监听 + 节流

在 Intersection Observer API 出现之前,开发者通常通过监听 scroll 事件来实现懒加载。

基本思路

  • 给图片设置 data-src 存储真实地址,src 先放一张极小的占位图(如 base64 或 1x1 像素图)。
  • 监听 scroll 事件,在回调中计算每张图片是否进入视口。
  • 如果图片进入视口,则将 data-src 的值赋给 src,完成加载。

示例代码(简单版)

<img class="lazy" data-src="real-image.jpg" src="placeholder.jpg" alt="">
const lazyImages = document.querySelectorAll('.lazy');

function isInViewport(el) {
  const rect = el.getBoundingClientRect();
  return rect.top < window.innerHeight && rect.bottom > 0;
}

function lazyLoad() {
  lazyImages.forEach(img => {
    if (isInViewport(img) && img.src !== img.dataset.src) {
      img.src = img.dataset.src;
    }
  });
}

window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);

存在的问题

  • 性能问题scroll 事件触发非常频繁(每滚动一像素都可能触发),频繁执行 getBoundingClientRect 会导致重排,影响滚动流畅度。
  • 需要手动节流:通常要配合 requestAnimationFrame 或 lodash 的 throttle 来降低回调执行频率。
  • 代码复杂度高:还要考虑动态添加的图片、浏览器兼容性等。

虽然可行,但不够优雅。现代浏览器提供了更专业的解决方案。

三、现代方案:Intersection Observer API

IntersectionObserver 是浏览器原生提供的异步观察 API,用于监听元素与其祖先元素或视口的交叉状态。它的出现完美解决了滚动监听的性能痛点。

核心概念

  • 观察者:创建 IntersectionObserver 实例,传入回调函数和配置项。
  • 被观察者:一个或多个 DOM 元素。
  • 交叉比(Intersection Ratio):元素可见比例,当比例超过阈值(threshold)时触发回调。
  • 触发时机:元素进入或离开视口时,回调函数会异步执行,不会阻塞主线程。

API 简介

const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 元素进入视口
    }
  });
}, {
  threshold: 0.5,   // 可见度达到 50% 时触发
  rootMargin: '0px' // 扩大或缩小视口区域
});

observer.observe(targetElement);   // 开始观察
observer.unobserve(targetElement); // 停止观察

四、实战:基于 Intersection Observer 的懒加载

下面我们结合你提供的完整代码,一步步实现一个可用的懒加载示例。

1. HTML 结构

我们需要一个足够长的页面来模拟滚动,同时为每张图片设置 class="lazy",用 src 放占位图,data-src 放真实图片地址。

<div class="box"></div>
<img class="lazy" src="placeholder.png" data-src="real-image-1.jpg" alt="">
<div class="box"></div>
<img class="lazy" src="placeholder.png" data-src="real-image-2.jpg" alt="">

为了滚动效果,我们给 .box 设置了 height: 200vh,这样两个盒子之间就会产生足够的滚动空间。

2. CSS 样式

简单重置内外边距,让占位盒子撑开页面:

* {
  margin: 0;
  padding: 0;
}
.box {
  height: 200vh;
  background-color: #f5f5f5;
}

3. JavaScript 懒加载逻辑

// 获取所有需要懒加载的图片
const images = document.querySelectorAll('.lazy');

// 创建 IntersectionObserver 实例
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    // 判断元素是否进入视口
    if (entry.isIntersecting) {
      const img = entry.target;
      const realSrc = img.dataset.src;
      
      // 将真实地址赋给 src,触发浏览器加载图片
      img.src = realSrc;
      
      // 图片开始加载后,取消观察,避免重复加载
      observer.unobserve(img);
    }
  });
});

// 对所有 .lazy 图片启动观察
images.forEach(img => observer.observe(img));

代码解析

  • observer 的回调接收 entries 数组,包含所有被观察元素的变化情况。
  • 我们遍历 entries,检查每个元素的 isIntersecting 属性,为 true 表示元素当前出现在视口中。
  • dataset.src 中取出真实图片地址,赋值给 src,图片开始加载。
  • 调用 observer.unobserve(img) 取消对该图片的观察,避免后续滚动重复触发。

4. 完整效果演示

4.1 HTML 例子

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片的懒加载</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        .box {
            height: 200vh;
            background-color: white;
        }
    </style>
</head>
<body>
    <div class="box"></div>
    <!-- 数据属性 -->
     <img class="lazy" src="https://img10.360buyimg.com/wq/jfs/t24601/190/890984006/4559/731564fc/5b7f9b7bN3ccd29ab.png" data-src="https://img.36krcdn.com/hsossms/20260119/v2_53cad3f2226f48e2afc1942de3ab74e4@5888275@ai_oswg1141728oswg1053oswg495_img_png~tplv-1marlgjv7f-ai-v3:960:400:960:400:q70.jpg?x-oss-process=image/format,webp" alt=""/>
    <div class="box"></div>
    <img class="lazy" src="https://img10.360buyimg.com/wq/jfs/t24601/190/890984006/4559/731564fc/5b7f9b7bN3ccd29ab.png" data-src="https://img.36krcdn.com/hsossms/20260117/v2_1e74add07bb94971845c777e0ce87a49@000000@ai_oswg421938oswg1536oswg722_img_000~tplv-1marlgjv7f-ai-v3:960:400:960:400:q70.jpg?x-oss-process=image/format,webp" alt=""/>
    <script>
         const images=document.querySelectorAll(".lazy") 
        const observer=new IntersectionObserver((entries)=>{
            console.log(entries)
            entries.forEach(entry=>{
                if(entry.isIntersecting){
                    const img=entry.target
                    const original_img=img.dataset.src
                    console.log(original_img)
                    img.src=original_img
                    observer.unobserve(img)
                }
            })
        })
     images.forEach(img=>observer.observe(img)) 
    </script>
</body>
</html>

补充

在代码中,两个 console.log 的作用是帮助开发者观察懒加载的触发过程。当你滚动页面,某张图片进入视口时,控制台会依次打印:

  1. entries:一个包含 IntersectionObserverEntry 对象的数组。
    每个对象都描述了被观察元素与视口的交叉状态,包括:

    • target:触发加载的 <img> 元素本身。
    • isIntersecting:当前元素是否与视口相交(此时为 true)。
    • intersectionRatio:可见比例(通常在 0~1 之间)。
    • boundingClientRectrootBounds 等位置信息。

    实际打印时,你会看到类似这样的输出(简化后):

    [IntersectionObserverEntry]
        0: IntersectionObserverEntry
          isIntersecting: true
          target: img.lazy
          ...
    
  2. original_img:从当前图片的 data-src 属性中读取的真实图片地址。
    例如,代码中第一张图片的 data-src 是一个 URL: img.36krcdn.com/hsossms/202… 控制台会直接打印这个字符串。

效果图

屏幕录制 2026-03-26 144452.gif

当页面加载时,由于两个 .box 占据了大量空间,图片可能位于视口之外,此时 src 仍是占位图(例如京东的默认图)。
当用户滚动到图片附近时,浏览器会自动检测到该图片进入视口,并动态将 data-src 的值赋给 src,图片开始加载。

这种方式的性能远优于 scroll 监听,因为所有计算都由浏览器底层在恰当的时候异步完成,不会阻塞主线程。

五、优化与注意事项

1. 占位图的重要性

占位图不宜过大,否则失去了“先加载轻量内容”的意义。通常使用:

  • 极小的 base64 图片,如 data:image/svg+xml,%3Csvg...
  • 纯色背景 + CSS 背景色占位
  • 低分辨率缩略图(LQIP 技术)

2. 避免重复加载

每次图片加载完成后,务必调用 unobserve。否则,如果用户反复滚动经过该图片,isIntersecting 可能多次为 true,导致重复赋值(虽然再次赋值不会重新下载,但仍浪费了判断成本)。

3. 兼容性与 Polyfill

Intersection Observer 在主流浏览器中的支持度已经非常好(除 IE 外)。如果需要兼容低版本浏览器,可以引入官方的 polyfill

<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>

4. 扩展:与动态加载结合

如果页面中有通过 AJAX 动态添加的图片,需要在新增图片后,再次调用 observer.observe(newImg),确保新图片也能被懒加载。

5. 配置项进阶

你可以通过 thresholdrootMargin 来控制图片加载的时机,例如提前加载:

const observer = new IntersectionObserver(callback, {
  rootMargin: '200px', // 视口上下各扩展 200px,图片距离视口还有 200px 时就开始加载
  threshold: 0
});

这样可以实现“预加载”效果,让用户滚动时感觉更流畅。

六、总结

图片懒加载是一个看似简单却蕴含深厚性能优化思想的实践。通过本文,我们对比了传统的滚动监听方案与现代的 Intersection Observer 方案,并基于你提供的代码完成了完整的懒加载实现。

方案优点缺点
scroll 监听兼容性好,原理简单性能差,需手动节流,代码复杂
Intersection Observer性能优秀,使用简洁,异步回调需考虑 polyfill(IE 不支持)

在实际项目中,推荐优先使用 Intersection Observer,它能让代码更清晰、性能更可控。如果你正在开发一个图片密集型页面,不妨立即动手试试这种懒加载方式,相信你的页面加载速度会有明显提升。