好的,关于 Web 前端常见的图片懒加载(Lazy Loading)实现方式,可以从“原生支持”和“手动实现”两个大的维度来理解。我会为你梳理几种主流的方法,并分析各自的优缺点,最后给出一个当前最佳实践的建议。
主流的图片懒加载方式
图片懒加载的核心思想是:仅在图片进入浏览器可视区域(Viewport)时,才去加载它。这极大地提升了页面的初始加载速度和用户体验,特别是对于图片密集型的网站。
以下是几种常见的方式:
1. HTML 原生懒加载 (loading="lazy")
这是目前最简单、也是最推荐的方式。通过为 <img> 或 <iframe> 标签添加 loading="lazy" 属性,浏览器会自动处理懒加载逻辑,无需任何 JavaScript 代码。
实现方式:
HTML
<img src="image.jpg" loading="lazy" alt="描述文字" width="200" height="200">
优点:
-
极致简单:只需添加一个属性,零 JavaScript 成本。
-
性能优秀:由浏览器原生实现,比 JavaScript 的解决方案性能更好,更省电。浏览器能够更智能地决定加载时机,比如会在用户即将滚动到图片前提前加载,体验更平滑。
-
兼容性好:目前主流的现代浏览器(Chrome, Firefox, Edge, Safari)均已支持。
缺点:
-
可控性弱:无法精确控制加载时机(比如提前多少像素加载),也无法添加自定义的加载动画(如淡入效果),一切由浏览器决定。
-
对旧浏览器不兼容:在不支持此属性的浏览器(如 IE11)上,图片会立即加载,相当于没有懒加载效果。但它会优雅降级,不会导致功能异常。
2. Intersection Observer API (交叉观察器)
这是目前最现代、最高效的 JavaScript 实现方式。它提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态变化的方法。简单来说,就是能告诉你一个元素是否进入了可视区域。
实现原理:
-
初始时,
<img>标签的src属性不放真实的图片地址,而是放在一个自定义属性中,例如data-src。src可以留空或指向一个极小的占位图(placeholder)。 -
创建一个
IntersectionObserver实例,并让它观察所有需要懒加载的图片元素。 -
当
IntersectionObserver的回调函数触发,判断图片元素是否进入可视区域(isIntersecting为true)。 -
如果是,则将
data-src的值赋给src属性,开始加载真实图片。 -
图片加载完成后,停止对该元素的观察,以释放资源。
示例代码:
JavaScript
document.addEventListener("DOMContentLoaded", function() {
const lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
if ("IntersectionObserver" in window) {
let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.classList.remove("lazy");
lazyImageObserver.unobserve(lazyImage); // 停止观察
}
});
});
lazyImages.forEach(function(lazyImage) {
lazyImageObserver.observe(lazyImage);
});
} else {
// Fallback for older browsers
// 可以选择在这里实现下面的传统事件监听方式
}
});
HTML
<img data-src="real-image.jpg" class="lazy" alt="描述" width="200" height="200">
优点:
-
性能好:相比传统的事件监听方式,
IntersectionObserver不会因频繁触发scroll事件而导致性能问题。它的回调是异步触发的,不阻塞主线程。 -
功能强大:API 提供了丰富的配置项,比如可以设置
rootMargin来提前或延迟加载(例如,当图片距离视窗底部还有200px时就开始加载),还可以通过threshold控制元素可见度达到多少比例时触发。 -
代码优雅:逻辑清晰,易于维护。
缺点:
- 存在兼容性问题:不兼容 IE 等非常陈旧的浏览器。但可以通过引入 Polyfill 来解决。
3. 传统事件监听 (scroll, resize, orientationchange)
在 IntersectionObserver 出现之前,这是最主流的实现方式。通过监听页面的滚动和尺寸变化事件,来判断元素是否进入可视区域。
实现原理:
-
与
IntersectionObserver类似,将真实图片地址存放在data-src中。 -
监听
window的scroll、resize等事件。 -
在事件处理函数中,获取所有待加载图片的位置信息(使用
getBoundingClientRect())。 -
遍历图片,计算其顶部是否小于等于视窗的高度,如果是,则说明图片已进入或即将进入可视区域。
-
将
data-src赋值给src,并移除该图片的事件监听或标记,避免重复计算。
优点:
- 兼容性极好:几乎兼容所有浏览器。
缺点:
-
性能开销大:
scroll事件触发非常频繁,如果在其回调函数中执行大量的计算(如循环、获取元素位置),很容易引发页面卡顿,造成不好的用户体验。 -
需要节流/防抖(Throttle/Debounce):为了缓解性能问题,必须对事件处理函数进行节流或防抖操作,这增加了代码的复杂性。
如何选择?
在当下的开发环境中,选择哪种方式取决于你的项目需求和目标用户群体。
-
首选推荐:原生 loading="lazy"
对于绝大多数场景,这都是最佳选择。它提供了足够好的性能和体验,且实现成本为零。对于不支持的旧浏览器,它会自动降级为普通图片加载,功能上没有问题,只是没有优化效果。你可以将其作为基础,再为不支持的浏览器提供 IntersectionObserver 作为增强方案。
-
次优选择(也是当前最佳的 JS 方案):IntersectionObserver
当你需要比原生懒加载更多的控制权时,比如:
-
需要实现自定义的加载动画(如淡入、占位符切换)。
-
需要更精确地控制预加载的距离。
-
不仅是图片,还需要对组件、广告等其他类型的 DOM 元素进行懒加载。
-
需要兼容一些不支持
loading="lazy"但支持IntersectionObserver的浏览器版本(虽然这个区间很小)。
-
-
最后的备选方案:传统事件监听
只有当你的项目必须兼容非常古老的浏览器(比如 IE8-11),且无法或不想引入 IntersectionObserver 的 Polyfill 时,才考虑使用这种方式。在现代项目开发中,它已基本被 IntersectionObserver 替代。
总结与最佳实践
综合来看,我认为最佳实践是 “原生懒加载为主,Intersection Observer 为辅” 的混合策略。
你可以这样做:
-
为所有
<img>标签默认加上loading="lazy"。 -
通过 JavaScript 检测浏览器是否支持
loading="lazy"。 -
如果不支持,则启动
IntersectionObserver的方案来作为 Polyfill(向下兼容的替代方案),实现懒加载。
这种方式结合了两者的优点,既能为现代浏览器提供最佳性能,又能确保在旧浏览器上懒加载功能依然可用,实现了优雅降级和渐进增强。许多成熟的第三方懒加载库,如 lazysizes,内部也是采用类似的智能检测和回退机制。