广告

JavaScript 实现图片懒加载:从原理到代码的前端性能优化实战

1. 懒加载的原理与优化目标

1.1 懒加载的核心原理

在网页加载阶段,图片资源往往占据较大体积,直接加载会阻塞渲染并增加网络请求。懒加载的核心思路是:只有当图片进入可视区域或接近可视区域时,才触发实际的网络请求来获取图片数据,从而降低初次加载时的带宽压力提升第一渲染速度。这一过程的关键在于将图片的真实地址放在一个可观察的属性中,等待触发条件再进行替换。

原理要点:通过对图片的可视区域检测、设置触发阈值以及在加载完成后清理资源,达到仅在需要时才加载的效果。

1.2 视口检测与性能指标

实现懒加载时,最常用的检测手段是IntersectionObserver,它可以在目标进入或离开视口时回调,做到时机精准、低开销。通过设置rootMargin,可以让图片在进入视口前就提前加载,以对准用户的滚动节奏,提升用户感知的流畅度。

性能指标方面,懒加载的目标是降低初始渲染阻塞、提升首屏加载速度图片资源的实际加载时序,进而改善LCP与TTI等关键指标。实现时记得保持代码简洁、避免过度监听造成的性能损耗。

2. 浏览器原生懒加载与降级方案

2.1 原生 lazy 属性的使用场景

现代浏览器对图片提供了原生的loading="lazy"属性,简单直接地将图片进入文档时的加载行为设为惰性。它的优点是无需额外 JavaScript,实现快速上手;但同时也存在局限,如对老浏览器支持不足,以及对可见性触发时机的灵活控制有限。

在实际项目中,可以用简单的方式配合占位图或占位样式来改善用户体验,同时为旧浏览器提供降级方案以确保功能可用性。

2.2 兼容性降级策略与回退方案

对于不支持 IntersectionObserver 的浏览器,可以提供传统事件驱动的懒加载或直接加载的回退方案,例如在页面滚动时逐步检查图片是否处于可视区域,从而触发加载。同时应为图片设置合适的<占位内容替代文本,确保无障碍与 SEO 的基本需求。

组合使用时,可以让原生 lazy 与自定义懒加载共存:若浏览器支持 IntersectionObserver,则优先使用自定义实现,否则退回到简单的滚动监听或直接加载。这样可实现广泛兼容性与一致体验。

3. 使用 IntersectionObserver 实现图片懒加载

3.1 基本实现思路

核心思路是:将图片的真实地址放在自定义数据属性中,如 data-src 或 data-srcset;通过 IntersectionObserver 观察图片是否进入视口,一旦进入则把数据属性的值赋给实际的 src(或 srcset),完成图片加载,并对已加载的图片停止观察以节省资源。

关键步骤:选择图片集合、创建观察器、设置合理的 rootMargin 与阈值、在回调中完成替换并清理绑定,确保内存与事件处理的高效。

3.2 处理多属性图片与占位策略

对于使用 srcset 的响应式图片,需要同时处理 data-src 和 data-srcset,确保在加载时正确切换到合适的分辨率。对占位图,可以使用低分辨率占位、渐变背景或占位图片,以避免页面空白对用户体验的冲击。

另外,为了提升可访问性,可以在图片原始的 alt 属性之外,保留对 lazy 过程的可理解文本描述,避免仅通过视觉效果传达信息,以便屏幕阅读器用户也能获得充分的信息。

4. 代码示例:完整实现

4.1 简易实现(基于 IntersectionObserver)

下面给出一个简易版本的完整实现,展示如何将 data-src 替换为实际 src,并在图片进入视口后加载。该示例适合快速集成到大多数静态页面中,且具有良好的向后兼容性。


document.addEventListener('DOMContentLoaded', function () {const lazyImages = document.querySelectorAll('img[data-src]');const config = {rootMargin: '100px 0px',threshold: 0.01};if ('IntersectionObserver' in window) {const observer = new IntersectionObserver(function (entries, self) {entries.forEach(entry => {if (entry.isIntersecting) {const img = entry.target;img.src = img.dataset.src;if (img.dataset.srcset) {img.srcset = img.dataset.srcset;}img.onload = function () {img.classList.add('loaded');};img.removeAttribute('data-src');img.removeAttribute('data-srcset');self.unobserve(img);}});}, config);lazyImages.forEach(img => observer.observe(img));} else {// 浏览器不支持 IntersectionObserver,回退到一次性加载lazyImages.forEach(img => {img.src = img.dataset.src;if (img.dataset.srcset) {img.srcset = img.dataset.srcset;}img.removeAttribute('data-src');img.removeAttribute('data-srcset');});}
});

4.2 进阶:结合原生 loading 与 data-src 的混合策略

在某些场景中,结合原生 loading 属性可以进一步简化实现,同时保持对旧浏览器的兼容性。下方代码展示了如何在支持 IntersectionObserver 的情况下仍然使用 data-src 的延迟加载,同时对不支持的环境回退到加载占位图后再替换。


(function () {const imgs = document.querySelectorAll('img[data-src]');if ('loading' in HTMLImageElement.prototype) {// 浏览器原生 lazy loading 支持imgs.forEach(img => {img.src = img.dataset.src;if (img.dataset.srcset) img.srcset = img.dataset.srcset;img.loading = 'lazy';img.removeAttribute('data-src');img.removeAttribute('data-srcset');});} else if ('IntersectionObserver' in window) {const io = new IntersectionObserver((entries, obs) => {entries.forEach(e => {if (e.isIntersecting) {const i = e.target;i.src = i.dataset.src;if (i.dataset.srcset) i.srcset = i.dataset.srcset;i.onload = () => i.classList.add('loaded');i.removeAttribute('data-src');i.removeAttribute('data-srcset');obs.unobserve(i);}});}, { rootMargin: '100px 0px', threshold: 0.01 });imgs.forEach(img => io.observe(img));} else {// 最后降级方案:直接加载所有图片imgs.forEach(img => {img.src = img.dataset.src;if (img.dataset.srcset) img.srcset = img.dataset.srcset;img.removeAttribute('data-src');img.removeAttribute('data-srcset');});}
})();

5. 性能评估与实践要点

5.1 加载时序优化与节流策略

通过将图片加载从“页面渲染直接触发”转变为“按需触发”,可以显著改善首屏渲染时间最大渲染内容时间。在实现中应关注避免频繁触发加载回调,可以通过节流/取消的策略来降低浏览器工作量,确保滚动期间的性能稳定。

另外,为了降低资源浪费,应尽量在图片进入视口前保持一个合理的 rootMargin,避免过早加载或过晚加载造成的体验问题。

5.2 与 SEO、无障碍的共存要点

懒加载若处理不当,可能影响图片的搜索引擎抓取与无障碍访问。关键要点包括:为图片提供完整的 alt 文本、确保占位内容具有意义、在可访问的情况下仍能逐步加载图片以及在需要时保留原始的图片地址以便爬虫理解资源路径。

通过结合原生 loading 属性、明确的替换逻辑与合理的降级策略,可以在保持 SEO 友好性的同时实现前端性能的显著提升。

JavaScript 实现图片懒加载:从原理到代码的前端性能优化实战

广告