骨架节点必须与真实dom完全同构,否则hydration错位导致ssr失败、内容闪动、焦点丢失;须在标签名、class名、嵌套层级、子节点数量四方面严格对齐,并用contain:layout防重排。

骨架节点必须和真实 DOM 完全同构,否则 hydration 会错位
Vue/React 的 SSR 水合失败、内容闪动、焦点丢失,90% 出在结构不一致。服务端输出的骨架 HTML 和客户端挂载的真实 DOM 必须在四个维度上严格对齐:标签名、class 名(大小写、连字符、命名空间)、嵌套层级、子节点数量。
常见错误包括:
- 真实卡片用
<article class="card"><header><h2></h2></header><figure><img></figure></article>,骨架却只写<div class="skeleton-card"></div> - 服务端模板里用
class="card {{ loading ? 'skeleton' : '' }}"动态拼接,导致服务端永远只有card,客户端 hydrate 时却期望card skeleton - 真实组件有 3 个
<li>,骨架只渲染 2 个或靠 JS 动态生成
正确做法是:骨架也用 <article class="card skeleton-card"></article>,真实内容用 <article class="card real-card"></article>,靠 CSS 控制显隐,但 DOM 结构不变。
图片与文字占位必须脱离内容依赖,靠 CSS 显式撑开空间
空元素默认渲染为 0×0,height: 100% 在父容器无高时完全失效——这不是“看起来没效果”,而是布局塌陷,真实内容插入后必然重排。
实操要点:
使用 Puppeteer + Chrome 将 HTML 渲染为中文 PDF,自动处理图表等待、Tab 展开、动画、测高、白边消除、防分页,适用于看板、报表、网页和交互图表转 PDF。
- 图片占位优先用
aspect-ratio: 16 / 9+width: 100%+overflow: hidden;旧浏览器 fallback 用padding-top: 56.25%+position: relative+ 绝对定位子元素 - 文字行不能靠
height,而要用line-height× 行数设定最小高度,例如三行标题设min-height: calc(1.5em * 3) - 禁用
<img src="data:image/svg+xml">或任何带 src 的占位图——它仍触发解码、占用主线程、无法精准控制尺寸
骨架必须随服务端首包 flush 输出,不能靠 JS 插入
骨架 HTML 如果没在 TTFB 后极短时间内到达浏览器,就等于没存在过。白屏时间直接拉长,LCP 指标劣化。
关键执行点:
- Node.js/Express 中禁用
ob_start()全量缓冲;改用res.write('<div id="root"></div>')+res.flush()提前下发主容器 - Next.js 的
loading.tsx只管客户端 hydration 后过渡;根 layout 的骨架 HTML 必须静态可预测,不能依赖请求数据 - PHP 模板中必须配
ob_flush()+flush(),否则骨架卡在 buffer 里——这是白屏超 2s 最常见原因 - @keyframes 动画必须内联在
<style></style>中,不能靠 JS 注入;CSS 自定义属性(--skeleton-color)禁用,服务端无法预计算
替换必须用 replaceChildren(),且父容器加 contain: layout
用 innerHTML = '' 清空再塞新内容,或用 display: none 切换,都会触发强制同步重排,造成“咔哒一跳”。
真正平滑的替换方式:
- 现代浏览器直接用
el.replaceChildren(...newNodes)—— 原子操作,保留 focus、事件监听器、CSS 动画上下文 - Safari 等旧环境需手动模拟:先
while (el.firstChild) el.removeChild(el.firstChild),再el.append(...newNodes)(注意:会丢失焦点) - 所有骨架元素默认设
opacity: 0.8+transition: opacity 0.25s ease-out;真实内容插入后立即设opacity: 1并移除skeleton类 - 父容器必须加
contain: layout—— 这不是可选项,是防止外部样式变化(如字体加载、图片回填)引发重排的最后一道防线
最常被忽略的是 contain: layout。即使结构同构、替换用 replaceChildren()、动画只走 opacity,没有它,骨架过渡依然可能抖动。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










