一、图片懒加载及 Intersection Observer 简介

1.1 图片懒加载的概念

在网页里,如果有大量图片,一次性全部加载,会让网页打开速度变得特别慢,用户可能要等好久才能看到完整页面。图片懒加载就是解决这个问题的办法。它的原理是只在图片快要进入浏览器的可视区域,或者已经进入可视区域的时候才去加载图片。这样一来,一开始网页加载的时候,就不用加载那些还看不到的图片,能大大加快网页的加载速度,提升用户体验。

比如说,一个电商网站有很多商品图片,要是不使用懒加载,用户打开页面就得等所有图片都加载完。但用了懒加载,用户打开页面,首先看到的是可视区域内的图片,滚动页面时,其他图片才会陆续加载。

1.2 Intersection Observer 的作用

Intersection Observer 是浏览器提供的一个 API,它能很方便地实现图片懒加载。它可以观察一个元素和它的祖先元素或者视口(浏览器显示网页的窗口)是否有相交部分。简单来说,就是能知道某个元素什么时候进入或者离开可视区域。

在图片懒加载中,我们可以用 Intersection Observer 来观察图片元素,当图片快要进入可视区域时,就触发加载图片的操作。

1.3 使用 Intersection Observer 实现图片懒加载示例

// 首先获取所有需要懒加载的图片元素
const lazyImages = document.querySelectorAll('img[data-src]');

// 创建一个 IntersectionObserver 实例
const imageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            // 如果图片进入了可视区域
            const img = entry.target;
            // 将 data-src 属性的值赋给 src 属性,触发图片加载
            img.src = img.dataset.src;
            // 停止观察该图片元素
            observer.unobserve(img);
        }
    });
});

// 对每个需要懒加载的图片元素进行观察
lazyImages.forEach(image => {
    imageObserver.observe(image);
});

上面的代码里,首先获取所有带有 data-src 属性的图片元素。data-src 是自定义属性,用来存放图片的真实地址。然后创建了一个 IntersectionObserver 实例,当观察的元素进入可视区域时,就把 data-src 的值赋给 src 属性,触发图片加载,并且停止对该图片的观察。

二、被忽略的滚动性能反杀案例

2.1 案例场景描述

有一个新闻资讯网站,页面里有大量的图片文章。为了提升页面加载速度,使用了 Intersection Observer 来实现图片懒加载。在测试环境或者页面图片数量较少的时候,一切都正常,滚动页面很流畅。

但是,当网站正式上线,真实用户访问,页面内容增多,图片数量变多之后,就出现问题了。用户滚动页面时,感觉很卡顿,滚动不流畅,影响了用户体验。

2.2 性能问题分析

经过分析发现,问题出在 IntersectionObserver 的回调函数里。当页面有大量图片时,每次滚动页面,IntersectionObserver 会频繁触发回调函数。因为它会检查每个观察元素与视口的相交情况,一旦有图片元素进入或者离开可视区域,就会调用回调函数。

在回调函数里,代码可能会进行一些复杂的操作,比如修改 DOM 元素、加载图片等。这些操作会占用大量的 CPU 资源,导致页面滚动时卡顿。

2.3 代码复现

// 获取所有需要懒加载的图片元素
const lazyImages = document.querySelectorAll('img[data-src]');

// 创建一个 IntersectionObserver 实例
const imageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            // 模拟一个复杂的操作,比如修改 DOM 结构
            const newDiv = document.createElement('div');
            newDiv.textContent = 'This is a test div';
            img.parentNode.insertBefore(newDiv, img);
            // 将 data-src 属性的值赋给 src 属性,触发图片加载
            img.src = img.dataset.src;
            // 停止观察该图片元素
            observer.unobserve(img);
        }
    });
    // 模拟一个耗时的计算
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
        sum += i;
    }
});

// 对每个需要懒加载的图片元素进行观察
lazyImages.forEach(image => {
    imageObserver.observe(image);
});

在这个代码里,当图片进入可视区域时,除了加载图片,还会在图片前面插入一个新的 div 元素,并且进行一个耗时的计算。当页面有大量图片时,每次滚动页面,这些操作都会频繁执行,导致性能问题。

三、解决滚动性能问题的方法

3.1 优化回调函数

我们要尽量简化 IntersectionObserver 的回调函数。只保留必要的操作,比如加载图片,避免在回调函数里进行复杂的 DOM 操作和耗时的计算。

// 获取所有需要懒加载的图片元素
const lazyImages = document.querySelectorAll('img[data-src]');

// 创建一个 IntersectionObserver 实例
const imageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            // 将 data-src 属性的值赋给 src 属性,触发图片加载
            img.src = img.dataset.src;
            // 停止观察该图片元素
            observer.unobserve(img);
        }
    });
});

// 对每个需要懒加载的图片元素进行观察
lazyImages.forEach(image => {
    imageObserver.observe(image);
});

和之前的代码相比,这里去掉了插入 div 元素和耗时计算的操作,只保留了加载图片的核心操作,这样能大大减少回调函数的执行时间,提高滚动性能。

3.2 节流和防抖

节流是指在一定时间内,只执行一次函数。防抖是指在一定时间内,如果再次触发函数,就重新计时,直到时间结束才执行函数。

我们可以使用节流和防抖来控制 IntersectionObserver 回调函数的执行频率。

// 节流函数
function throttle(func, delay) {
    let timer = null;
    return function() {
        if (!timer) {
            func.apply(this, arguments);
            timer = setTimeout(() => {
                timer = null;
            }, delay);
        }
    };
}

// 获取所有需要懒加载的图片元素
const lazyImages = document.querySelectorAll('img[data-src]');

// 创建一个 IntersectionObserver 实例
const imageObserver = new IntersectionObserver(throttle((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            // 将 data-src 属性的值赋给 src 属性,触发图片加载
            img.src = img.dataset.src;
            // 停止观察该图片元素
            observer.unobserve(img);
        }
    });
}, 200));

// 对每个需要懒加载的图片元素进行观察
lazyImages.forEach(image => {
    imageObserver.observe(image);
});

在这个代码里,使用了节流函数 throttle 来包装 IntersectionObserver 的回调函数。这样,在 200 毫秒内,回调函数只会执行一次,减少了不必要的执行次数,提高了性能。

3.3 分批次加载

如果页面图片特别多,可以考虑分批次加载。比如,每次只加载可视区域内和附近的图片,等用户滚动到一定位置,再加载下一批图片。

// 获取所有需要懒加载的图片元素
const lazyImages = Array.from(document.querySelectorAll('img[data-src]'));
let currentIndex = 0;
const batchSize = 10;

// 创建一个 IntersectionObserver 实例
const imageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            // 将 data-src 属性的值赋给 src 属性,触发图片加载
            img.src = img.dataset.src;
            // 停止观察该图片元素
            observer.unobserve(img);
        }
    });
    // 加载下一批图片
    const nextBatch = lazyImages.slice(currentIndex, currentIndex + batchSize);
    nextBatch.forEach(image => {
        imageObserver.observe(image);
    });
    currentIndex += batchSize;
});

// 先加载第一批图片
const initialBatch = lazyImages.slice(0, batchSize);
initialBatch.forEach(image => {
    imageObserver.observe(image);
});

在这个代码里,把图片分成一批一批加载。每次只观察当前批次的图片,当观察到图片进入可视区域时,加载图片并停止观察。然后加载下一批图片,继续观察。这样可以避免一次性观察大量图片,减少性能开销。

四、应用场景

4.1 新闻资讯网站

新闻资讯网站通常有大量的文章和图片。使用 Intersection Observer 实现图片懒加载,能让页面快速加载,用户可以更快地看到新闻内容。同时,优化滚动性能,可以让用户在浏览新闻时滚动页面更加流畅。

4.2 电商网站

电商网站的商品列表页有大量的商品图片。图片懒加载可以减少页面初始加载时间,提高用户体验。而解决滚动性能问题,可以让用户在浏览商品列表时,不会因为滚动卡顿而影响购物体验。

4.3 社交网站

社交网站的动态列表页有很多图片,比如用户发布的照片、视频封面等。通过实现图片懒加载和优化滚动性能,可以让用户更快地看到动态内容,并且在浏览动态时更加流畅。

五、技术优缺点

5.1 优点

使用 Intersection Observer 实现图片懒加载有很多优点。首先,实现起来比较简单,不需要像传统方法那样监听滚动事件,处理复杂的滚动位置计算。其次,性能较好,它是浏览器原生的 API,能高效地检测元素与视口的相交情况。最后,兼容性也不错,大部分现代浏览器都支持。

5.2 缺点

Intersection Observer 也有一些缺点。比如,它依赖于浏览器的支持,如果是一些老旧的浏览器,可能无法使用。另外,如果处理不当,像我们前面提到的案例,会出现滚动性能问题。

六、注意事项

6.1 兼容性处理

在使用 Intersection Observer 时,要考虑到不同浏览器的兼容性。可以使用一些 polyfill 来解决不兼容的问题。

# 安装 intersection-observer polyfill
npm install intersection-observer
// 在代码中引入 polyfill
import 'intersection-observer';

6.2 回调函数优化

一定要注意回调函数的优化,避免在回调函数里进行复杂的操作。可以将一些不必要的操作放到回调函数外面执行。

6.3 资源回收

当图片加载完成后,要及时停止对图片元素的观察,避免不必要的资源消耗。

七、文章总结

在网页开发中,使用 Intersection Observer 实现图片懒加载是一个很好的优化手段,能显著提升页面加载速度和用户体验。但是,我们不能忽视它可能带来的滚动性能问题。通过优化回调函数、使用节流和防抖、分批次加载等方法,可以有效解决这些性能问题。

同时,在实际应用中,我们要考虑到应用场景、技术的优缺点,并且注意兼容性处理、回调函数优化和资源回收等问题。只有这样,才能充分发挥 Intersection Observer 的优势,为用户提供更加流畅的网页体验。