🔥掌握图片懒加载:从 onscroll 的“蹒跚学步”到 Intersection Observer 的“优雅起舞”

100 阅读6分钟

嘿,朋友们!

你是否曾有过这样的经历:你精心设计了一个内容丰富的网站,却发现它在用户手中变得卡顿无比,尤其是在图片众多的页面上?当用户滚动页面时,那迟滞的感觉,仿佛每滑动一帧都在与浏览器进行一场艰苦的拔河比赛。

这,就是性能优化的战场。而在这场战斗中,图片懒加载,是每一位前端工程师都必须掌握的核心技能。

今天,我们不谈枯燥的理论。我将带你用讲故事的方式,亲历懒加载技术的进化史。我们将从那个曾经被我们依赖,但又有些“笨拙”的 onscroll 方案开始,最终迎接我们今天的主角——Intersection Observer,看看这位“现代英雄”是如何用优雅的身姿,轻松解决这个经典难题的。

准备好了吗?让我们开始这场性能优化之旅吧!

🤔 第一站:为什么我们需要懒加载?

想象一下,浏览器就像一位任劳任怨的建筑工人。当它拿到你的 HTML 图纸时,会从上到下开始施工。当它看到 <img> 标签,它的反应非常直接:“停下所有事,马上去下载这张图!”

如果页面只有三五张图,这没什么问题。但如果这是一个电商网站,有上百件商品呢?

灾难就发生了:浏览器会瞬间开启上百个下载任务,把网络带宽挤得水泄不通。结果就是,用户在很长一段时间内看到的只是一个空白的页面和旋转的加载图标,体验极差。

懒加载的哲学非常简单,却无比强大:

不要一次性加载所有东西。只加载用户当前能看到的内容,其他的,等用户滚动到了再说!

为此,我们约定了一个简单的“障眼法”:

  1. 图片的真实地址,我们不放在 src 属性里,而是藏在一个自定义的 data-original 属性中。
  2. src 属性里放一个超小的占位图 (loading.gif 或透明像素点)。

这样,页面就能瞬间加载完成。接下来,我们的任务就是:如何聪明地知道图片进入了用户的视野?


🐢 第二站:传统方案 onscroll —— 勤奋但低效的“保安”

在前端世界的早期,我们最直观的办法就是派一个“保安”去时刻盯着。这个保安,就是 window.onscroll 事件。

它的工作模式是这样的:

把它想象成一个极度负责但有点“神经质”的保安。他不会只在有人进门时才检查,而是每时每刻都在门口来回踱步,对每一个从门口经过的路人都要盘问一遍:“你要进来了吗?现在进来了吗?”

这反映在代码里就是:

window.addEventListener('scroll', () => {
  // 每滚动一下,就检查页面上所有的图片
  const lazyImages = document.querySelectorAll('img[data-original]');
  
  lazyImages.forEach(img => {
    // getBoundingClientRect() 用来获取图片相对于视口的位置
    const rect = img.getBoundingClientRect();
    const viewportHeight = document.documentElement.clientHeight;

    // 如果图片进入了视口...
    if (rect.top < viewportHeight) {
      // ...就开始加载真实图片
      img.src = img.dataset.original;
      // 记得移除标记,下次就不用再检查它了
      img.removeAttribute('data-original');
    }
  });
});

这个“保安”的问题在哪?

  1. 性能极差scroll 事件的触发频率高得惊人。用户稍微动一下滚轮,这个检查函数就会被执行几十次,造成巨大的计算浪费。
  2. 强制回流 (Reflow):在循环中调用 getBoundingClientRect() 是性能的头号杀手。为了获取最精确的位置,它会强制浏览器立即重新计算页面布局,打断正常的渲染流程,导致页面卡顿。

为了优化,我们不得不给这个“保安”吃“镇定剂”——也就是我们熟知的节流 (throttle)防抖 (debounce) 函数。但这就像是给一个有问题的系统打补丁,终究不是完美的解决方案。


🚀 第三站:现代英雄 Intersection Observer —— 优雅而高效的“狙击手”

终于,浏览器开发者们也受不了这种笨拙的方式了。他们为我们提供了一个原生的、高性能的解决方案——Intersection Observer(交叉观察器)。

它的工作模式完全不同:

它不再是那个来回巡逻的保安,而是一位优雅的“狙击手”。你只需要告诉他要监视哪些目标(图片)。他会静静地待在原地,只有当目标进入他预设的“瞄准镜”(视口)时,他才会扣动扳机,通知你:“目标已出现!”

整个过程由浏览器在底层高效完成,完全不占用你的主线程时间。

看看代码变得多么简洁:

document.addEventListener('DOMContentLoaded', () => {
    const lazyImages = document.querySelectorAll('img[data-original]');

    // 1. 创建我们的“狙击手”
    const observer = new IntersectionObserver((entries) => {
        // entries 是所有状态发生变化的被观察者数组
        entries.forEach(entry => {
            // 如果 entry.isIntersecting 为 true,说明目标进入了视口
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.original;
                img.removeAttribute('data-original');

                // ✨ 关键一步:一旦加载,就停止观察,节省资源
                observer.unobserve(img);
            }
        });
    });

    // 2. 告诉“狙击手”,需要监视哪些目标
    lazyImages.forEach(img => {
        observer.observe(img);
    });
});

Intersection Observer 的绝对优势:

  • 🏆 性能卓越:告别 scroll 监听和 getBoundingClientRect(),性能与原生无异。
  • 🧠 代码简洁:逻辑清晰,易于维护,不再需要节流防抖。
  • ✨ 体验更佳:可以利用 rootMargin 选项,让图片在进入视口前 提前 加载,实现真正的无缝丝滑体验。

🎯 终点站:一目了然的对决

特性onscroll 方案Intersection Observer 方案
性能🔴 差 (高频事件 + 强制回流)🟢 优秀 (原生、异步)
代码复杂度🟠 较高 (需要节流/防抖)🟢 极简 (声明式 API)
功耗耗电,尤其在移动端节能,对移动设备友好
推荐度⭐⭐⭐⭐⭐

你的回合:现在就去升级你的代码库!

朋友,技术的进步就是为了让我们能用更优雅的方式,解决更复杂的问题。

Intersection Observer 就是这样一个典型的例子。它不仅是实现图片懒加载的 最佳实践,更能用于实现无限滚动、内容曝光统计等多种与“可见性”相关的强大功能。

现在,就去检查一下你的项目吧!把那些还在使用 onscroll 的旧代码,升级为 Intersection Observer。这不仅是一次简单的代码重构,更是你作为一名前端工程师,对卓越用户体验和极致性能追求的体现。

你的用户,和你的 Lighthouse 跑分,都会感谢你的!

希望这篇文章对你有帮助。如果你喜欢这种讲解方式,请给我一个赞吧!Happy Coding!