大家好,今天,咱们来聊聊一个在面试中几乎“必考”的经典问题:常见图片懒加载方式有哪些? 别小看这个问题——它在面试中的出现频率很高!为什么?因为懒加载(Lazy Loading)是前端性能优化的核心技能之一,面试官用它来“一箭三雕”:
在深入原理前,先看看懒加载的典型场景。想象一下:你打开一个电商App,首页有100张商品图。如果全加载,用户得盯着空白屏等10秒!懒加载在这里大显身手:只加载首屏图片,用户往下滚时,才动态加载新图片。这不仅让页面“秒开”,还减少了服务器压力(比如带宽节省30%以上)。常见场景包括:
接下来进入正题——懒加载的实现原理。
Intersection Observer API是浏览器原生提供的接口,用来高效监测目标元素(比如图片)是否进入或离开视口(viewport,即用户当前看到的屏幕区域)。它基于“观察者模式”——你注册一个“观察员”(observer),浏览器自动在后台计算元素位置,触发回调。相比传统方法,它更省资源,因为避免了频繁的滚动事件计算。
Intersection Observer API
是浏览器提供的原生接口,用于高效、异步地监测目标元素与其祖先元素或顶级文档视口(Viewport)的交叉状态(Intersection)。它采用观察者模式,开发者注册一个观察器(Observer),浏览器引擎负责在后台计算目标元素的可见性变化,并在满足阈值条件时触发回调函数。
核心优势:
getBoundingClientRect()
带来的布局抖动(Layout Thrashing)和性能开销,由浏览器底层优化计算。root
)、预加载边距(rootMargin
)、可见性触发阈值(threshold
)。// 步骤1:创建 IntersectionObserver 实例
const imgObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
// 当元素进入视口(或满足阈值条件)
if (entry.isIntersecting) {
const lazyImage = entry.target;
// 将 data-src 存储的真实URL赋给 src 属性,触发图片加载
lazyImage.src = lazyImage.dataset.src;
// 可选:移除 data-src 属性或添加标记,避免重复处理
lazyImage.removeAttribute('data-src');
// 图片加载成功后,停止观察该元素
observer.unobserve(lazyImage);
}
});
}, {
root: null, // 观察相对于视口 (null 或未指定即默认为视口)
rootMargin: '0px 0px 200px 0px', // 观察区域向外扩展(下边扩展200px,实现提前加载)
threshold: 0.1 // 目标元素10%可见时触发回调(可设为数组[0, 0.1, 0.5, 1]监测多个阈值点)
});
// 步骤2:获取所有需懒加载的图片元素(使用 data-src 而非 src)
const lazyImages = document.querySelectorAll('img[data-src]');
// 为每个图片元素注册观察
lazyImages.forEach(image => {
imgObserver.observe(image);
});
data-src
属性:初始 HTML 中,图片的src
属性设置为一个轻量占位符(如 1x1 透明像素图)或为空,将真实图片 URL 存储在data-src
属性中。这是懒加载的标准实践模式。IntersectionObserver
回调:当被观察元素 (entry.target
) 的交叉状态满足threshold
条件时(entry.isIntersecting === true
),执行加载逻辑。data-src
的值赋给src
属性,浏览器自动发起图片请求。unobserve
):加载完成后调用unobserve()
解除对该元素的观察,避免不必要的后续计算。rootMargin
):设置rootMargin: '0px 0px 200px 0px'
表示观察区域向下扩展 200px。这使得图片在滚动到距离视口底部还有 200px 时就开始加载,实现平滑的“预加载”效果,提升用户体验。threshold
控制触发回调的可见比例阈值。loading="lazy"
:HTML 提供了原生懒加载属性 (<img src="image.jpg" loading="lazy" alt="...">
)。其兼容性在快速提升,但尚未完全普及(尤其旧版浏览器)。可作为渐进增强方案与 JS 实现结合使用。Intersection Observer
方案提供了更精细的控制能力。threshold
):可设置为数组(如[0, 0.25, 0.5, 0.75, 1]
),在元素达到不同可见比例时多次触发回调,适用于复杂场景(如元素进入/离开视口的不同动画效果)。getBoundingClientRect()
(兼容方案)这种方式基于事件监听——给window添加scroll事件,当用户滚动时,用getBoundingClientRect()
方法计算每个图片相对于视口的位置。如果图片进入或接近视口,则动态设置src属性加载图片。它兼容性好(支持所有浏览器),但性能开销大,因为scroll事件触发频繁,需要手动优化(如节流)。
此方案是传统的实现方式,其核心逻辑是:
window
的scroll
事件(以及resize
事件)。Element.getBoundingClientRect()
方法计算每个元素相对于视口的位置。核心价值:
主要缺点:
scroll
和resize
事件触发频率极高,频繁调用计算密集型方法getBoundingClientRect()
会导致严重的性能问题(重排 Reflow),造成页面卡顿。// 步骤1:定义懒加载检查函数
function lazyLoadTraditional() {
const lazyImages = document.querySelectorAll('img[data-src]'); // 获取待加载图片
lazyImages.forEach(img => {
// 获取元素相对于视口的边界矩形
const rect = img.getBoundingClientRect();
// 判断元素是否进入视口 (上边界 < 视口高度 且 下边界 > 0)
if (rect.top < window.innerHeight && rect.bottom > 0) {
img.src = img.dataset.src; // 加载图片
img.removeAttribute('data-src'); // 移除标记,避免重复处理
}
});
}
// 步骤2:添加带节流优化的滚动事件监听器
let isThrottled = false;
const throttleDelay = 200; // 节流延迟时间(ms)
window.addEventListener('scroll', () => {
if (!isThrottled) {
isThrottled = true;
// 使用 setTimeout 实现基础节流
setTimeout(() => {
lazyLoadTraditional();
isThrottled = false;
}, throttleDelay);
}
});
// 添加 resize 事件监听(同样需要节流)
window.addEventListener('resize', () => {
if (!isThrottled) {
isThrottled = true;
setTimeout(() => {
lazyLoadTraditional();
isThrottled = false;
}, throttleDelay);
}
});
// 步骤3:初始化 - 页面加载时检查首屏图片
document.addEventListener('DOMContentLoaded', lazyLoadTraditional);
// 或确保在页面初始布局完成后执行
window.addEventListener('load', lazyLoadTraditional);
getBoundingClientRect()
:返回元素的大小及其相对于视口的位置(top
,right
,bottom
,left
,width
,height
)。top
是元素顶部到视口顶部的距离(视口坐标系,上正下负)。rect.top < window.innerHeight
表示元素顶部在视口底部上方(即已进入或部分进入视口)。rect.bottom > 0
表示元素底部在视口顶部下方(防止元素完全在视口上方时误判)。两者结合确保元素与视口有交叉。throttleDelay
, 如 200ms)只执行一次lazyLoadTraditional
函数,大幅减少getBoundingClientRect()
的调用频率。示例使用了简单的setTimeout
节流。更优方案是使用requestAnimationFrame
结合标志位或 lodash 的_.throttle
。resize
事件:窗口大小改变也会影响元素是否在视口内,因此也需要监听并节流处理。DOMContentLoaded
或load
),必须主动调用一次lazyLoadTraditional
加载首屏图片。懒加载不是“可有可无”,而是前端性能的“守门员”。两种主流方式各有千秋。
核心原则:在现代浏览器项目中,优先使用 Intersection Observer API
;对于需要兼容旧浏览器的场景,使用基于滚动事件的方案并务必实施节流优化。
注意,懒加载不是万能药,过度使用可能影响SEO(搜索引擎爬虫可能不执行JS),所以要用<img>
标签配合原生属性(如loading="lazy"
)作为补充。未来,随着浏览器进化,原生懒加载会更普及,但原理不变——面试时,需要抓住“性能优化”这根主线。
面对“图片懒加载”相关问题,建议结构化回答:
清晰定义与价值:
“图片懒加载是一种延迟加载非可视区域图片资源的技术,核心目标是优化首屏加载性能(提升 FCP/LCP)、节省带宽、改善用户体验(尤其在长列表/图片密集页面和弱网环境)。”
阐述主流方案与对比:
“主要有两种实现方式:首选是现代浏览器支持的Intersection Observer API,它性能高效、配置灵活、代码简洁;其次是兼容性更好的传统方案,通过监听scroll
/resize
事件并结合getBoundingClientRect()
计算元素位置,但必须使用节流优化性能。”
深入核心实现细节:
“使用Intersection Observer的关键步骤是:1) 创建observer
实例,配置rootMargin
(如提前200px加载) 和threshold
;2) 用querySelectorAll
获取带data-src
的图片;3) 在回调中检测entry.isIntersecting
,若为true
则将data-src
赋给src
并调用unobserve()
。代码大致如... [简述关键代码行]。”
提及优化与注意事项:
“实践中需注意:使用data-src和占位符、设置图片尺寸避免布局偏移、考虑兼容性和 Polyfill、进行错误处理。还需关注对 SEO 的潜在影响,可通过<noscript>
标签或结合原生loading="lazy"
属性缓解。”
面试就像打游戏——这个问题是“小Boss”,答好了就能升级!关键态度是需要展现对原理的清晰理解、对不同方案的优缺点认知、对实际工程细节(性能、兼容性、SEO)的考量,以及将理论落地的能力。保持专业、自信、条理清晰。
这篇文章就到这里。如果你有更多问题,欢迎留言讨论——我是你们的面试战友,下期见!(别忘了转发给正在求职的小伙伴,一起上岸!)
写在最后