骨架图占位阶段HTML结构不规范是CLS最直接隐蔽根源;服务端骨架须与客户端DOM同构,标签名、class、子节点数量、data属性必须严格一致;图片文字占位需确定性尺寸声明;骨架DOM必须卸载而非隐藏;父容器须加contain: layout锁定布局边界。

骨架图占位阶段的 HTML 结构不规范,是布局移位(CLS)最直接、最隐蔽的根源——不是 CSS 写得不好,而是浏览器从第一帧解析起就没收到确定性信号。
服务端骨架必须与客户端真实 DOM 同构
hydration 错位不是“样式没对齐”,而是 DOM 树在服务端和客户端根本不是同一棵树。React/Vue 会直接丢弃整个节点重挂载,导致闪屏、表单清空、焦点丢失。
- 标签名必须一致:
<header>不能换成<div>,<figure>不能降级为<div> - class 名大小写、连字符、顺序全等:
card-title≠CardTitle≠title-card - 子节点数量必须严格相等:骨架有 3 个
<div class="skeleton-item">,真实内容也必须是 3 个<article>,多一个注释或空格文本节点都算错位 - data 属性必须对齐:
data-id="123"、aria-label等若真实节点有,骨架里就得有,否则框架判定为“非同一节点”
图片与文字占位必须靠原生尺寸或 CSS 确定性声明
浏览器解析 HTML 的首帧只信任 width/height 属性或 aspect-ratio,CSS 的 max-width: 100% 或 height: 100% 在父容器无高时完全失效,骨架直接坍缩为 0×0,后续插入真实内容必然触发 layout shift。
- 图片占位优先用
aspect-ratio: 16 / 9+width: 100%+overflow: hidden - 兼容旧浏览器用
padding-top: 56.25%+position: relative+ 子元素position: absolute撑满 - 禁用
height: 100%和<img src="data:image/svg+xml">——后者仍触发解码,拖慢主线程 - 文字行高用固定值:
height: 48px+line-height: 24px(对应 2 行),而非height: 100%或依赖 font-metrics 自动计算
骨架 DOM 必须卸载,不能隐藏
v-show、display: none、visibility: hidden、opacity: 0 都是陷阱——它们让骨架节点持续参与布局计算、占用无障碍树、干扰 IntersectionObserver,更致命的是破坏 hydration 一致性。
立即学习“前端免费学习笔记(深入)”;
- 真实内容加载完成,必须调用
element.remove()彻底销毁骨架节点 - 若用
replaceWith(),传入值必须是DocumentFragment或真实 DOM 节点,禁止字符串拼接(会丢失事件监听器) - 推荐现代方案:
parent.replaceChildren(realNode)—— 原子操作,保留 focus、不触发强制重排 - 服务端输出的骨架节点应带明确标识,如
data-skeleton="true",便于 JS 精准定位并清理
父容器必须加 contain: layout
即使骨架和真实内容结构完全同构、替换用 replaceChildren()、动画用 opacity,只要父容器没加 contain: layout,外部字体加载、图片尺寸回填、第三方脚本注入等任何变化,都可能引发整块区域重排。
-
contain: layout告诉浏览器:“这个容器内部的布局变化,不会影响外部” - 它锁住布局边界,是骨架过渡真正稳定的最后一道防线
- 该声明需写在骨架和真实内容的**共同父容器**上,不是骨架节点自身
- 不支持该属性的旧浏览器(如 Safari ≤15.3)可配合
position: relative+z-index做轻量降级,但无法替代 containment 效果
真正难的不是写出骨架,而是在 SSR hydration、CSS containment、移动端弱网、无障碍读屏之间维持零偏差——任何一处松动,在高并发或低端设备上都会被瞬间放大成用户可感知的抖动或白屏断点。



















