replaceChildren()是唯一能避免“咔哒一跳”的替换方式:它原子性替换子节点,不触发强制重排,保留焦点、事件和动画上下文;配合contain: layout与严格同构骨架结构,才能实现真正平滑过渡。

replaceChildren() 是唯一能避免“咔哒一跳”的替换方式
骨架屏消失时出现突兀跳动,90% 是因为用了 innerHTML = '' 清空再写入、或用 display: none 切换显隐。这些操作会触发强制同步重排,浏览器必须重新计算整个容器的布局。
现代浏览器支持原生 replaceChildren(),它原子性地替换子节点:父容器不销毁、焦点不丢失、已绑定事件监听器保留、CSS 动画上下文不中断——这对平滑过渡至关重要。
- 只在真实内容完全准备好后调用
el.replaceChildren(...newNodes),不要提前清空 - 旧版 Safari(≤ 15.3)需手动模拟:
while (el.firstChild) el.removeChild(el.firstChild)+el.append(...newNodes),但会丢失 focus 和事件 - 别在骨架节点上提前绑
click或mouseenter,否则替换瞬间可能触发两次
opacity 过渡必须配合 contain: layout 才不抖
只写 opacity: 0 → 1 不够。骨架和真实内容高度不一致时,即使 opacity 平滑,下方元素仍会因布局塌陷而跳动——这不是动画问题,是布局隔离没做好。
contain: layout 告诉浏览器:“这个容器内部的尺寸变化,不会影响外部”。字体加载、图片回填等扰动,就不会再让骨架区域抖动。
立即学习“前端免费学习笔记(深入)”;
- 给骨架和真实内容的**直接父容器**加
contain: layout(关键!漏掉这行,90% 的跳动都白调) - 骨架元素设
opacity: 0.8+transition: opacity 0.25s ease-out - 真实内容插入后立即设
opacity: 1,并移除所有skeleton类名 - 禁用
will-change: opacity——低端设备上反而引发额外合成层开销
骨架结构必须和真实 DOM 严格同构
不是“长得像”,而是层级数量、class 名、data-testid 等属性必须逐字对齐。否则 diff 引擎(包括手写 JS 替换逻辑)会误判为全新节点,导致重绘、事件丢失、SSR hydration 失败。
- 骨架用
<div class="title"></div>,真实内容就得是<div class="title">xxx</div>,不能换成<h2> - 列表项骨架数量要按最大预期生成(如服务端直出 5 个
<div class="item">),客户端再整组替换 - 漏写
data-skeleton或多写data-loaded都会导致结构错位
服务端直出骨架是前提,JS 激活只是补位
等 JS 加载完再 document.createElement() 插入骨架,用户看到的是“空白→骨架→内容”,三段式闪烁。首屏感知时间已经晚了。
正确做法是骨架结构随 HTML 一起下发,且初始状态为可见+占位。JS 的作用只是在数据就绪后,用 replaceChildren() 原子替换。
- 骨架必须放在
<body>开始处,紧随<body>标签之后,早于真实内容容器 - Vue/React 中不要靠
loading === true控制显隐;应判断data === null || data.length === 0 - SSR 场景下 hydration 时若 document.readyState === 'complete',需跳过初始骨架显示,否则会闪一下
contain: layout 和结构同构这两点。它们不产生视觉效果,但一旦缺失,所有 CSS 动画和 JS 替换都会失效。



















