SSR样式抖动根本原因是服务端与客户端在类名生成、样式注入或hydration时机任一环节断开:类名不匹配(如Emotion cache key不一致或CSS Modules哈希配置不同)、服务端未调用extractCritical注入style到head、或hydrate过早于CSS解析完成。

SSR 出现 CSS 样式抖动(FOUC 或 layout shift),根本不是“样式没加载完”,而是服务端与客户端在类名生成、样式注入、hydration 时机三个环节中任一断开——只要 class 名不一致、style没塞进 head、或 hydrate 发生在 CSS 解析完成前,抖动就必然发生。
类名不匹配:CSS-in-JS 或 CSS Modules 两端哈希对不上
服务端输出 Button__click___a1b2c,客户端 hydration 后生成 Button__click___x9y8z,React 判定 DOM 不匹配,丢弃服务端 HTML 重绘一遍。这不是警告,是强制重排。
- Emotion 必须保证服务端和客户端用完全相同的
createCache({ key: 'ssr' }),key 缺失、拼错、或版本混用(v11 vs v12)都会导致哈希算法不同 - CSS Modules 在 Vite 中必须显式配置
css.modules.generateScopedName: '[name]__[local]___[hash:base64:5]',不能依赖默认值或仅 dev 生效;若启用css.lightningcss,还得同步设scopeBehaviour: 'local' - 禁用
css.modules.symbols——它在 SSR 场景下会引入不可控的模块 ID,极易引发 cache 错配
服务端样式没注入:HTML 里压根没有对应 <style></style>
Emotion / Styled Components 的规则默认只存在内存 cache 里,不会自动变成字符串塞进 HTML。extractCritical(v11)或 renderStylesToString(v12+)调用被跳过,等于服务端“白渲”。
- Express/Next.js SSR 入口里,每次请求必须新建 cache 实例,不能写在模块顶层被复用
-
CacheProvider必须包裹整个<app></app>树,漏掉任意一层组件(尤其是用keyframes的),该部分样式就收不到 - 最终 HTML 的
中必须包含提取出的css字段内容,且需验证其中是否存在@keyframes css-xyz { }—— 没有说明动画规则未收集
客户端 hydration 过早:样式还没解析完就强行挂载
即使类名一致、<style></style> 已内联,如果 hydrateRoot 在 document.styleSheets 的 cssRules 还不可读时执行,浏览器仍会先按无样式渲染,再重绘,造成肉眼可见的闪。
- 不要在
DOMContentLoaded里触发 hydrate;应监听<link rel="stylesheet">的onload回调,再调用 hydrate - 若使用提取出的静态 CSS(如
ssr-styles.css),必须以同步<link>方式加载,禁用preload+ 异步切换,否则 FOUC 不可避免 - 检查
@media规则是否被完整提取(如暗色模式相关样式),缺失会导致主题切换时二次抖动
真正容易被忽略的是:抖动从不单独发生。它总是暴露更底层的问题——cache 配置漂移、构建与运行时环境不一致(NODE_ENV)、或第三方库在服务端偷偷读取了 window。定位时别只盯着 CSS,先确认 hydration error 是否静默发生,再逐层比对两端的 cache key、哈希输入、样式注入路径。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











