因为<img>是替换元素,:before/:after在其上默认不渲染,即使设置content:""和背景也无效;需用容器包裹并对其使用伪类,配合aspect-ratio、loading="lazy"及JS控制loaded状态实现渐进式占位。

为什么不能直接用 :before 或 :after 给 <img> 添加占位背景?
因为 <img> 是替换元素(replaced element),:before 和 :after 在它上面默认不渲染——即使你写了 content: "" 并设置了宽高和背景,浏览器也会忽略。这不是 bug,是规范行为。所以常见错误是写了一堆 img::before { background: #f0f0f0; } 却完全没效果。
真正可行的方案是:把 <img> 包进一个容器(比如 <div> 或 <figure>),再对这个容器使用伪类占位。
- 容器必须设置
display: block(或inline-block),且明确宽高(或通过aspect-ratio控制比例) -
::before需要content: ""+display: block才能撑开并渲染背景 - 图片加载完成后,用 JS 移除容器上的占位 class,让
::before失效,同时显示真实图片
如何用 loading="lazy" 配合伪类做渐进式占位?
现代浏览器原生支持 loading="lazy",但它不提供占位能力。你需要手动补足视觉反馈。推荐结构如下:
<div class="img-placeholder" style="aspect-ratio: 16/9;"> <img src="photo.jpg" loading="lazy" alt="..."> </div>
对应 CSS:
立即学习“前端免费学习笔记(深入)”;
.img-placeholder {
position: relative;
background: #eee;
}
.img-placeholder::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: placeholder-shimmer 2s infinite;
}
.img-placeholder.loaded::before {
display: none;
}
@keyframes placeholder-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
- 动画依赖
background-position变化,比纯色更显“加载中”状态 -
aspect-ratio替代固定 height,适配响应式布局(IE 不支持,需 fallback) - JS 监听
img.onload后给父容器加loadedclass,触发伪类隐藏 - 别忘了监听
img.onerror,失败时也加loaded并显示 fallback 文案
伪类占位在 Safari 和旧版 Chrome 的兼容性坑
Safari 15.4 之前不支持 aspect-ratio,而老 Chrome(background-size 动画支持不稳定。实际部署时必须处理降级:
- 用
@supports (aspect-ratio: 1/1)包裹现代写法,否则 fallback 到min-height: 200px - 动画闪烁问题:Safari 中
background-position动画可能卡顿,可改用opacity淡入(但失去 shimmer 效果) - 伪类内容被读屏器读出:添加
aria-hidden="true"到容器,避免干扰无障碍访问 - 如果项目需支持 IE,伪类方案彻底失效,只能回退到
<svg>占位或 JS 插入<div>
用 IntersectionObserver 触发占位切换的边界条件
别等图片进入视口才开始加载——那样用户会先看到空白。应该在图片距离视口还有一定距离时就触发加载,同时立即显示占位。
- 设置
rootMargin: "100px",让图片提前 100px 开始加载 - 观察器回调里,先给容器加
loadingclass(启用 shimmer),再设置img.src(如果是 data-src) - 注意:如果图片已缓存,
onload可能同步触发,导致loadedclass 在伪类动画刚启动时就被移除——建议加setTimeout(() => ..., 16)做最小延迟保障动画可见 - 滚动极快时,可能连续触发多次
onload,需在回调里用img.dataset.loaded = "true"做幂等判断
伪类本身不处理加载逻辑,只负责视觉表达;真正的懒加载时机、资源请求、错误兜底,都得靠 JS 控制。混淆这两层职责,容易写出既不优雅又难调试的代码。


















