骨架结构必须随服务端HTML一同输出,禁止JS动态插入;应复用DOM节点并用CSS过渡实现平滑切换;响应式需用媒体查询而非JS重绘;加载完成后须调用remove()彻底清理。

骨架结构必须和服务端 HTML 一起输出,不能靠 JS 动态插入
骨架屏一旦靠 document.createElement 或 innerHTML 在 JS 加载后才插入 DOM,就必然错过首屏渲染时机。用户看到的是“白屏 → 骨架闪现 → 内容跳入”,CLS(累积布局偏移)直接超标。
真实项目中常见错误:Vue 的 v-if="loading" 放在组件顶层,但组件 mount 时数据还没 fetch,骨架晚于 HTML 解析;React 的 useEffect 里才 setLoading(true),此时页面已开始绘制空白区域。
- 服务端渲染(SSR)场景:骨架结构必须随初始 HTML 一同返回,且与真实 DOM 层级、类名、宽高声明完全一致
- 客户端渲染(CSR)场景:骨架需在
document.readyState === 'loading'阶段就写入document.body,或用requestIdleCallback微调插入时机 - 禁止在
mounted/useEffect中才创建骨架节点——它已经晚了
结构变化时的阻尼不是靠 JS 动画,而是靠 CSS 过渡 + 结构复用
所谓“阻尼”,本质是骨架消失、真实内容浮现时的视觉平滑感。很多人误以为要写 JS 控制 opacity 渐变或 transform 位移,其实真正有效的做法是复用骨架容器、仅切换子节点内容,并配以 opacity 和 transform 的 CSS 过渡。
例如:.skeleton-card 和 .card 必须是同一 DOM 节点,骨架用 background 占位,真实内容插入其内部;而不是两个平行节点靠 display: none/block 切换。
立即学习“前端免费学习笔记(深入)”;
- 过渡属性只设
opacity和transform,避免height、margin等触发布局计算 - 过渡时间控制在
0.2s–0.3s:太短无感,太长反显卡顿 - 真实内容插入前,先移除骨架的
animation,再触发过渡,否则动画和 fade-out 同时跑会冲突
响应式骨架结构切换必须用 CSS 媒体查询,而非 JS 重绘
移动端只显示标题+头像,桌面端显示完整卡片——这种结构差异如果靠 JS 检测 window.innerWidth 后删掉/重建 DOM,hydration 会失败,SSR 与 CSR 结构不一致,React/Vue 都可能报 key 错误或丢失状态。
正确做法是写一套全量骨架结构,用 @media 控制不同断点下的 display 或 visibility,所有节点始终存在、层级不变。
- 不要用
if (isMobile) renderSkeletonA() else renderSkeletonB() - 所有骨架元素加固定尺寸(如
width: 40px、aspect-ratio: 1/1),避免依赖父容器 flex 行为导致坍缩 - 移动端隐藏的段落用
@media (max-width: 768px) { .skeleton-paragraph { display: none; } },别用 JS toggle class
加载完成后的骨架清理必须调用 remove(),而非 display: none
很多项目把骨架节点设为 display: none 就完事,但它的 @keyframes 动画仍在后台运行,单页应用反复进入同一页面时,内存泄漏明显,Chrome DevTools 的 Performance 面板能看到持续的 style recalc。
尤其在 Vue 的 keep-alive 或 React 的 memo 组件中,骨架节点若没被真正销毁,下次激活还会重跑动画逻辑,甚至和新数据竞争 DOM。
- 务必在数据加载 resolve 后,对每个骨架节点调用
el.remove() - 如果骨架是通过模板字符串插入的,用
querySelectorAll('.skeleton-*')批量清理,别只清父容器 - 避免在
unmounted或useEffect cleanup里清理——此时可能已过期;应在数据 ready 的回调里立即执行
box-sizing,就会闪动。



















