图片懒加载本质上就是:
图片进入可视区域时再去加载,而不是页面一开始就把所有图片都请求下来。
这样做的好处:
- 减少首屏请求数
- 提升页面加载速度
- 节省带宽
- 提升用户体验
一、常见实现思路
一般有两种主流方案:
- 监听滚动 + 计算元素是否进入视口
- 使用
IntersectionObserver
其中现在更推荐:
IntersectionObserver
因为性能更好,写法也更简单。
二、方案一:滚动监听实现
HTML
先不要直接把真实图片地址放到src,而是放到data-src:
<img class="lazy" data-src="https://example.com/1.jpg" src="placeholder.jpg" /> <img class="lazy" data-src="https://example.com/2.jpg" src="placeholder.jpg" /> <img class="lazy" data-src="https://example.com/3.jpg" src="placeholder.jpg" />这里:
src="placeholder.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 实现
这是现在更推荐的方式。
HTML
<img class="lazy" data-src="https://example.com/1.jpg" src="placeholder.jpg" /> <img class="lazy" data-src="https://example.com/2.jpg" src="placeholder.jpg" /> <img class="lazy" data-src="https://example.com/3.jpg" src="placeholder.jpg" />JS
const 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 - 性能更好
- 代码更简洁
四、支持提前加载
有时候不希望图片完全进入屏幕才加载,而是:
快进入屏幕时就提前加载
可以用rootMargin:
const 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-lazy="'https://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 src="loading.png" data-src="real.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 class="lazy" data-src="real.jpg" src="placeholder.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实现。