CSS Modules在SSR中FOUC的根本原因是服务端与客户端generateScopedName不一致导致类名失配,需统一哈希逻辑、同步注入静态CSS并精确控制水合时机。

为什么CSS Modules在SSR中会FOUC
服务端渲染出的 HTML 带有类似 Button__primary___abc123 的类名,但客户端水合时若生成了不同哈希(比如 Button__primary___def456),样式就对不上——浏览器先用服务端类名+服务端内联样式显示,再被客户端新类名覆盖,造成视觉跳变。这不是“CSS没加载完”,而是两端 generateScopedName 不一致导致的类名失配。
必须让服务端和客户端生成完全相同的类名
关键不是“关掉哈希”,而是确保哈希逻辑在构建和运行时都严格一致:
-
css.modules.generateScopedName不能只在mode === 'development'下配置;必须在 Vite 的build和server阶段都生效 - 推荐显式写死格式:
generateScopedName: '[name]__[local]___[hash:base64:5]',避免依赖文件路径、时间戳或模块 ID - 若启用
css.lightningcss,必须同步设css.lightningcss.cssModules.scopeBehaviour: 'local',否则 Lightning CSS 会绕过 Vite 默认逻辑 - 禁用
css.modules.symbols(如设为true或自定义函数),它在 SSR 场景下极易引发缓存不匹配
提取静态 CSS 并同步注入 HTML
光类名一致还不够——服务端输出的样式必须随 HTML 一起到达,不能等 JS 水合后才注入:
- 用
static-style-extract(专为 SSR 设计)在构建阶段扫描所有服务端组件,输出ssr-styles.css - 该文件必须以
<link rel="stylesheet">形式同步注入<head>,不能preload后异步切换,否则仍 FOUC - 禁用组件级 CSS Modules 的动态注入:设
css.devSourcemap = false,并通过build.rollupOptions.output.manualChunks把模块样式全打进ssr-styles.css - 确保提取结果包含
@media规则(如@media (prefers-color-scheme: dark)),否则暗色模式切换时也会闪
水合时机比你想象的更关键
即使 CSS 文件已加载、类名完全一致,如果客户端 hydrateRoot 触发得太早,依然会闪:
立即学习“前端免费学习笔记(深入)”;
- 不要在
document.addEventListener('DOMContentLoaded')里 hydrate;应等document.styleSheets中关键 CSS 的cssRules可读后再执行 - 更稳妥的做法是:给
<link rel="stylesheet" href="ssr-styles.css">加id="ssr-styles",然后轮询检查document.getElementById('ssr-styles')?.sheet?.cssRules.length > 0 - 若使用 React + Next.js,需确认
useEffect不在首帧就修改 DOM;若用 SvelteKit,务必启用prepareStylesSSRhook


















