图片懒加载时,如何让加载状态延迟显示更自然?

♫建利 阅读 41

我在给图片列表加懒加载功能时遇到了问题,用loading="lazy"加上loading属性后,加载中的骨架屏总是在图片出现前0.5秒就消失了,看起来很突兀。我试过给骨架屏加固定延迟:


document.querySelectorAll('.image-item').forEach(item => {
  item.querySelector('img').addEventListener('load', () => {
    setTimeout(() => item.classList.remove('is-loading'), 800)
  })
})

但发现网络好的时候图片加载更快,导致延迟时间反而更长。如果网速慢,骨架屏又会在图片出现前就消失。有没有更动态的办法让加载状态在图片真正准备好后再消失?

我来解答 赞 15 收藏
二维码
手机扫码查看
2 条解答
Good“树甜
你这个思路其实方向是对的,但问题出在把“图片加载完成”和“视觉上看起来自然”混在一起考虑了。

核心问题在于:图片真正加载完的那一刻,如果立刻移除骨架屏,用户会感觉“咔一下”跳出来,尤其是网络快的时候,确实很突兀。但你加死延迟又会反向伤害慢速网络体验——这说明你需要的不是时间上的延迟,而是视觉过渡的缓冲。

推荐两个更自然的做法,按推荐顺序来:

第一种,也是最稳妥的:用 CSS 的 transition 配合 opacity 做淡入淡出,而不是靠 JS 控制时间。骨架屏和真实图片重叠在一起,用一个 class 控制可见性切换,让浏览器自己处理过渡。这样无论网络快慢,用户看到的都是平滑的fade out -> fade in。

比如 HTML 结构改成这样:
<div class="image-item">
<div class="skeleton"></div>
<img src="..." loading="lazy" class="real-img">
</div>


CSS 里:
.skeleton, .real-img {
transition: opacity 0.3s ease-out;
}
.image-item.is-loading .skeleton {
opacity: 1;
}
.image-item.is-loaded .skeleton {
opacity: 0;
pointer-events: none;
}
.image-item.is-loaded .real-img {
opacity: 1;
}
.real-img {
opacity: 0;
}


JS 只在图片 load 完后加个 is-loaded 类:
document.querySelectorAll('.image-item img').forEach(img => {
img.addEventListener('load', () => {
img.closest('.image-item').classList.add('is-loaded');
});
});


这样更清晰,而且你甚至可以微调 transition 的时间,让过渡更舒服。

第二种,如果必须用 JS 做延迟(比如你骨架屏里还有动画),那别用 setTimeout,改用 requestAnimationFrame 做“下一帧再消失”,配合一个最小延迟 + 最大延迟的组合判断:

const MIN_DELAY = 300;
const MAX_DELAY = 600;

document.querySelectorAll('.image-item img').forEach(img => {
const item = img.closest('.image-item');
let startTime = Date.now();

img.addEventListener('load', () => {
const elapsed = Date.now() - startTime;
const delay = Math.max(MIN_DELAY, Math.min(elapsed, MAX_DELAY));

setTimeout(() => {
item.classList.remove('is-loading');
}, delay);
});
});


这样网络快的时候,比如 100ms 就加载完了,也会至少再等 300ms 才消失;网络慢的时候,比如 800ms 才好,也不会等太久——它会“追着”加载时间走,但被限制在一个合理范围内。这样更自然,不会出现“图片出来了,骨架屏还在”的尴尬。

我一般更推荐第一种方案,因为可维护性高,而且不依赖 JS 的 timing,兼容性和性能都更稳。
点赞 2
2026-02-26 13:17
IT人诗晴
我的做法是监听图片的加载状态,同时结合自然的过渡效果来处理。你可以用 IntersectionObserver 来动态检测图片是否进入视口,然后再根据图片的实际加载情况来控制骨架屏的显示和隐藏。

具体来说,先给每个图片元素设置一个默认的加载状态样式,比如骨架屏。然后通过 IntersectionObserver 检测图片是否可见,当图片进入视口时,再绑定 load 事件监听器,确保图片完全加载完成后再移除骨架屏。

代码可以这样写:

document.querySelectorAll('.image-item img').forEach(img => {
let item = img.closest('.image-item');

// 使用 IntersectionObserver 检测图片是否进入视口
let observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 图片进入视口后绑定 load 事件
img.addEventListener('load', () => {
// 图片加载完成后移除骨架屏
item.classList.remove('is-loading');
});

// 如果图片已经缓存,直接移除骨架屏
if (img.complete) {
item.classList.remove('is-loading');
}

// 停止观察该图片
observer.unobserve(img);
}
});
}, { threshold: 0.1 }); // 当图片 10% 进入视口时触发

// 开始观察图片
observer.observe(img);
});


这个方法的好处是它不会固定延迟时间,而是根据图片的实际加载状态动态调整。如果图片加载得快,骨架屏会迅速消失;如果加载慢,骨架屏会一直保留到图片加载完成。另外,记得给 .is-loading 状态加一点 CSS 过渡效果,比如淡出动画,这样视觉体验会更自然。

对了,如果你发现某些图片在缓存情况下瞬间加载完成,也可以在代码里加上 img.complete 的判断,避免骨架屏闪一下就消失的情况。

希望这个方案能帮到你!
点赞 8
2026-02-16 07:01