图片懒加载本质上就是图片进入可视区域时再去加载而不是页面一开始就把所有图片都请求下来。这样做的好处减少首屏请求数提升页面加载速度节省带宽提升用户体验一、常见实现思路一般有两种主流方案监听滚动 计算元素是否进入视口使用IntersectionObserver其中现在更推荐IntersectionObserver因为性能更好写法也更简单。二、方案一滚动监听实现HTML先不要直接把真实图片地址放到src而是放到data-srcimg classlazy data-srchttps://example.com/1.jpg srcplaceholder.jpg / img classlazy data-srchttps://example.com/2.jpg srcplaceholder.jpg / img classlazy data-srchttps://example.com/3.jpg srcplaceholder.jpg /这里srcplaceholder.jpg占位图data-src真实图片地址JS 实现function lazyLoad() { const images document.querySelectorAll(img.lazy) images.forEach(img { const rect img.getBoundingClientRect() if (rect.top window.innerHeight rect.bottom 0) { const realSrc img.dataset.src if (realSrc) { img.src realSrc img.classList.remove(lazy) img.removeAttribute(data-src) } } }) } window.addEventListener(scroll, lazyLoad) window.addEventListener(resize, lazyLoad) window.addEventListener(DOMContentLoaded, lazyLoad)原理getBoundingClientRect()获取元素相对视口的位置。比如const rect img.getBoundingClientRect()如果rect.top window.innerHeight说明图片顶部已经进入视口底部以上了也就是快进入屏幕了。可以再加节流优化因为scroll触发很频繁所以一般要配合节流。function throttle(fn, delay) { let timer null return function () { if (timer) return timer setTimeout(() { fn() timer null }, delay) } } const betterLazyLoad throttle(lazyLoad, 200) window.addEventListener(scroll, betterLazyLoad) window.addEventListener(resize, betterLazyLoad) window.addEventListener(DOMContentLoaded, lazyLoad)三、方案二IntersectionObserver 实现这是现在更推荐的方式。HTMLimg classlazy data-srchttps://example.com/1.jpg srcplaceholder.jpg / img classlazy data-srchttps://example.com/2.jpg srcplaceholder.jpg / img classlazy data-srchttps://example.com/3.jpg srcplaceholder.jpg /JSconst images document.querySelectorAll(img.lazy) const observer new IntersectionObserver((entries, observer) { entries.forEach(entry { if (entry.isIntersecting) { const img entry.target const realSrc img.dataset.src if (realSrc) { img.src realSrc img.removeAttribute(data-src) img.classList.remove(lazy) } observer.unobserve(img) } }) }) images.forEach(img observer.observe(img))原理IntersectionObserver会帮你监听某个元素是否进入可视区域进入了多少什么时候离开视口当图片进入视口时entry.isIntersecting true就把img.src img.dataset.src这样浏览器才会真正发起图片请求。优点相比滚动监听它有这些优势不需要频繁手动计算不需要一直监听scroll性能更好代码更简洁四、支持提前加载有时候不希望图片完全进入屏幕才加载而是快进入屏幕时就提前加载可以用rootMarginconst observer new IntersectionObserver((entries, observer) { entries.forEach(entry { if (entry.isIntersecting) { const img entry.target img.src img.dataset.src observer.unobserve(img) } }) }, { root: null, rootMargin: 100px, threshold: 0 })这里表示距离视口还有100px时就开始加载这样用户滚到图片位置时图片往往已经加载好了。五、封装一个通用懒加载函数原生 JS 版function initLazyImages() { const images document.querySelectorAll(img[data-src]) if (!(IntersectionObserver in window)) { images.forEach(img { img.src img.dataset.src img.removeAttribute(data-src) }) return } const observer new IntersectionObserver((entries, obs) { entries.forEach(entry { if (!entry.isIntersecting) return const img entry.target const realSrc img.dataset.src if (realSrc) { img.src realSrc img.onload () { img.removeAttribute(data-src) } img.onerror () { console.log(图片加载失败) } } obs.unobserve(img) }) }, { rootMargin: 100px 0px }) images.forEach(img observer.observe(img)) }调用document.addEventListener(DOMContentLoaded, initLazyImages)六、Vue 里怎么做在 Vue 项目里常见是封装成一个自定义指令。Vue3 自定义指令实现export default { mounted(el, binding) { const observer new IntersectionObserver((entries) { entries.forEach(entry { if (entry.isIntersecting) { el.src binding.value observer.unobserve(el) } }) }, { rootMargin: 100px }) observer.observe(el) } }注册import { createApp } from vue import App from ./App.vue import lazy from ./directives/lazy const app createApp(App) app.directive(lazy, lazy) app.mount(#app)使用template img v-lazyhttps://example.com/test.jpg src/placeholder.png / /template七、React 里怎么做React 里一般封装成组件或 hook。简单版import { useEffect, useRef, useState } from react function LazyImage({ src, placeholder, alt }) { const imgRef useRef(null) const [currentSrc, setCurrentSrc] useState(placeholder) useEffect(() { const observer new IntersectionObserver(entries { entries.forEach(entry { if (entry.isIntersecting) { setCurrentSrc(src) observer.unobserve(entry.target) } }) }) if (imgRef.current) { observer.observe(imgRef.current) } return () observer.disconnect() }, [src]) return img ref{imgRef} src{currentSrc} alt{alt} / }八、实际项目中常见细节1. 占位图懒加载时通常会先显示一张占位图默认灰图loading 图模糊缩略图例如img srcloading.png data-srcreal.jpg /等真实图加载完成后再替换。2. 图片加载失败兜底img.onerror () { img.src error.png }避免图片裂开影响体验。3. 骨架屏 / 渐进加载真实项目里常配合骨架屏模糊图占位渐显动画比如图片加载完成后加个淡入img { opacity: 0; transition: opacity 0.3s; } img.loaded { opacity: 1; }img.onload () { img.classList.add(loaded) }4. 列表更新后的新图片如果页面是动态渲染的比如分页加载无限滚动瀑布流那么新插入的图片也需要重新observe。5. SSR 场景注意如果是服务端渲染项目要注意window不存在IntersectionObserver只能在客户端执行要放到挂载后执行。九、为什么懒加载能优化性能因为正常情况下页面里如果有 100 张图片浏览器一开始可能就会请求很多张首屏渲染变慢带宽占用变大图片很多时容易卡顿而懒加载只会先加载首屏附近的图片其他图片等滚动到时再加载所以首屏更快网络压力更小页面更流畅十、面试回答怎么说你可以这样答图片懒加载的核心思想是当图片未进入可视区域时不去加载真实图片只显示占位内容当图片进入或接近视口时再把真实地址赋值给img.src。实现方式早期通常是监听scroll事件结合getBoundingClientRect()判断元素是否进入视口现在更推荐使用IntersectionObserver由浏览器帮我们监听元素和视口的相交情况性能更好。实际项目中还会结合占位图、失败兜底、提前加载和动画优化体验。十一、最推荐的现代写法img classlazy data-srcreal.jpg srcplaceholder.jpg /const imgs document.querySelectorAll(.lazy) const observer new IntersectionObserver((entries, obs) { entries.forEach(entry { if (!entry.isIntersecting) return const img entry.target img.src img.dataset.src img.onload () img.classList.add(loaded) obs.unobserve(img) }) }, { rootMargin: 100px }) imgs.forEach(img observer.observe(img))十二、一句话总结图片懒加载就是先不加载真实图片等图片进入可视区域时再设置src推荐用IntersectionObserver实现。