一、图片懒加载及 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 的优势,为用户提供更加流畅的网页体验。
Comments