loading="lazy"仅控制加载时机,不能替代占位图;src必须存在(如base64或svg占位),否则导致布局跳动、seo下降;需结合aspect-ratio、css占位与js懒加载逻辑协同。

loading="lazy"不能替代占位图管理
它只控制加载时机,不提供占位逻辑——loading="lazy 会等浏览器判断进入视口才发请求,但在这之前 src 若为空或无效,图片就直接塌陷,页面布局跳动不可避免。
常见错误是只写 <img loading="lazy"> 却没设 src,结果首屏空白、高度丢失、SEO 友好性下降。现代浏览器不会自动 fallback 到 data-src,也不会渲染占位。
-
src必须存在(哪怕是个 1×1 的 base64 占位图或低质量缩略图) -
data-src仅作真实地址容器,和loading="lazy"无绑定关系 - 若同时用
loading="lazy"和 Intersection Observer,前者会被后者覆盖——因为 JS 手动赋值src后,浏览器不再按 lazy 策略重新介入
用 CSS aspect-ratio + placeholder 控制布局不跳动
光靠 JS 切换 src 不足以防止重排,关键在 DOM 渲染前就锁定尺寸。最稳妥的方式是:HTML 中声明宽高比,CSS 强制容器撑开空间,再用 background-image 或 src 替换内容。
例如:<img class="lazy" src="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20 style="max-width:90%"%20 style="max-width:90%"/>" width="600" height="400"> 配合:
img.lazy {
aspect-ratio: 600 / 400;
background-color: #f5f5f5;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'/>");
object-fit: cover;
}
- SVG 占位图体积小(
-
aspect-ratio在 Chrome 88+、Firefox 89+、Safari 15.4+ 均支持,比传统width/height更可靠(尤其响应式场景) - 避免用
min-height或 padding-top 百分比 hack,它们依赖父容器宽度,容易在 flex/grid 下失效
Intersection Observer 中如何安全替换 src 并保持占位状态
替换 src 不是原子操作:赋值瞬间可能触发 broken 状态、加载失败、或还没渲染完就滚动出视口。必须把占位状态作为中间态显式维护。
典型做法是加 class 控制视觉反馈,并在回调中分阶段更新:
function loadImg(img) {
const src = img.dataset.src;
if (!src) return;
// 进入加载态:移除占位背景,显示加载中样式
img.classList.remove('placeholder');
img.classList.add('loading');
img.src = src;
img.onload = () => {
img.classList.remove('loading');
img.classList.add('loaded');
};
img.onerror = () => {
img.classList.remove('loading');
img.classList.add('failed');
img.src = '/fallback.jpg'; // 降级图需确保存在且尺寸匹配
};
}
- 不要在
onload里直接改img.style.display,这会绕过 CSS transition;用 class 切换更可控 - 务必在
img.src = src前校验dataset.src,空字符串或相对路径错误会导致 404 请求,浪费资源 - 加载完成后建议删除
dataset.src,避免后续被重复读取或干扰其他逻辑
SPA 场景下 observer 失效与内存泄漏风险
单页应用中,组件频繁挂载/卸载,但 IntersectionObserver 实例若未手动 disconnect(),会持续监听已销毁的 DOM 节点,造成内存泄漏,且新插入的 .lazy 图片不会被自动观察。
正确做法是在组件卸载时清理,在新节点插入后重新 observe:
- 创建 observer 时保存引用,比如挂到组件实例上:
this.io = new IntersectionObserver(...) - 在 React useEffect cleanup、Vue onUnmounted、或原生 removeEventListener 时机调用
this.io.disconnect() - 动态插入图片后,需显式调用
observer.observe(img),不能依赖初始 DOM 查询 - 避免在 scroll 事件里反复调用
querySelectorAll('.lazy')—— 每次都新建 NodeList,性能差且易漏掉新节点
占位图逻辑本身不复杂,难的是在各种生命周期、异步加载、错误边界之间保持状态一致。最容易被忽略的不是“怎么换图”,而是“换图失败时用户看到什么”和“换完之后 DOM 还在不在”。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











