css modules在ssr中闪烁的根本原因是服务端与客户端生成的类名哈希不一致,如button__primary___abc123与button__primary___def456不匹配,导致html首次渲染后被客户端新类名覆盖而视觉跳变。

为什么CSS Modules在SSR中会闪烁
服务端渲染出的 HTML 带有类似 Button__primary___abc123 的类名,但客户端水合时若生成了不同哈希(比如 Button__primary___def456),样式就对不上——浏览器渲染两遍:先用服务端类名+服务端内联样式显示,再被客户端新类名覆盖,造成视觉跳变。根本不是“没加载完CSS”,而是服务端和客户端的 css.modules.generateScopedName 不一致。
Vite 6 中确保类名稳定的配置写法
必须让服务端和客户端使用完全相同的 scoped class 生成逻辑,否则提取出的静态 CSS 文件无法复用:
-
css.modules.generateScopedName不能留空或只在 dev 模式下设值;需在 SSR 构建和客户端运行时都生效 - 推荐显式固定哈希算法和上下文:
generateScopedName: '[name]__[local]___[hash:base64:5]',避免依赖文件路径或构建时间戳 - 若用了
css.lightningcss,必须同步配置css.lightningcss.cssModules.scopeBehaviour: 'local',否则 Lightning CSS 会绕过 Vite 默认逻辑,导致两端不一致 - 禁用
css.modules.symbols(如true或自定义函数),它在 SSR 场景下极易引发 cache 不匹配
提取静态 CSS 文件并注入 HTML 的实操要点
静态样式表不是“把所有 CSS 合并”,而是把服务端已知、且客户端水合时不会变更的样式提前固化:
- 用
static-style-extract工具(专为 SSR 设计)在构建阶段扫描所有服务端组件,输出ssr-styles.css - 该 CSS 必须在 HTML
中以<link rel="stylesheet">同步加载,不能preload后异步切换——否则仍会 FOUC - 同时禁用组件级 CSS Modules 的动态注入:在 Vite 配置中设置
css.devSourcemap = false和build.rollupOptions.output.manualChunks把模块样式全打进ssr-styles.css,防止客户端额外拉取 - 注意:提取结果要包含
@media规则(如@media (prefers-color-scheme: dark)),否则暗色模式切换时也会闪
容易被忽略的 hydration 时机陷阱
即使 CSS 文件正确加载、类名完全一致,如果客户端水合早于样式解析完成,依然会闪:
- 不要在
document.addEventListener('DOMContentLoaded')里触发hydrateRoot;应等document.styleSheets中关键 CSS 的cssRules可读后再 hydrate - 更稳妥的做法是:给
<link rel="stylesheet" href="ssr-styles.css">加onload回调,并在回调里调用 hydrate —— 但需加 3s 超时兜底,防 404 或网络中断 - Next.js App Router 下,
useClient组件若引用了未被static-style-extract捕获的样式(比如条件 import 的模块),必须手动import 'xxx.module.css'到根 layout,否则 SSR 阶段压根不会收集
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











