css modules 在 server component 中失效导致样式丢失,因服务端 styles 为空对象、class 名不一致引发 hydration 失败与视觉跳变;需确保 ssr 与客户端 class 名生成逻辑一致。

因为服务端生成的 class 名与客户端 hydration 时生成的不一致,导致样式规则丢失,浏览器回退到无样式状态——不是没加载 CSS,是 class 对不上。
Server Component 渲染时未注入 CSS Modules 样式
CSS Modules 的 import styles from './Button.module.css' 在 Server Component 中执行时,styles 是一个空对象(或 fallback 对象),不会触发 CSS 文件解析和 class 名生成。服务端根本没拿到真实 class 名,HTML 里写的 className={styles.button} 最终变成 className="" 或 className="undefined"。
- Next.js App Router 的 Server Component 默认不支持 CSS Modules 的运行时 class 映射,它只在 Client Component 中通过
use client启用后才生效 - 即使你把组件标记为 Client Component,若服务端仍参与了初始渲染(比如父组件是 Server Component 并透传 props),class 名可能被提前求值为空
- 构建产物中
.module.css文件虽存在,但服务端没走 Webpack/Vite 的 CSS Modules 处理流程,无法生成哈希化 class
Client Component hydration 时 class 名不匹配
客户端 JS 加载后,styles.button 被正确计算为 Button_button__abc123,但服务端吐出的 HTML 里对应元素是 class="button" 或空字符串。React hydration 发现 DOM 节点 class 不一致,会放弃复用、强制重渲染,造成视觉跳变。
- 常见错误:在 Server Component 中直接解构
import { button } from './Button.module.css'并用于 className —— 这在服务端求值为undefined - 更隐蔽的问题:使用
clsx(styles.button, props.className),当styles.button是undefined时,结果变成clsx(undefined, 'custom')→"custom",丢失模块化 class - 动态主题或条件 class(如
{styles[theme]})在服务端没有theme上下文,也会 fallback 到空值
构建配置未启用 SSR 友好 CSS Modules 支持
Vite 或 Webpack 若未显式配置 CSS Modules 的服务端兼容模式,会默认按客户端逻辑生成 class 名(例如依赖 process.env.NODE_ENV 或 runtime hash seed),而服务端环境缺少这些上下文,导致 class 名生成算法失准。
- Vite 用户需确认
css.modules.generateScopedName没有引用process.env或随机函数;推荐用静态格式如[name]_[local]_[hash:5],并在服务端渲染前预设一致的 hash seed - Webpack 用户检查
css-loader的modules.exportLocalsConvention和getLocalIdent是否稳定;避免用path.resolve动态拼接路径作为 localIdent 基础 - Next.js Pages Router 下,
next.config.js中若禁用了experimental.cssImport或覆盖了默认 css-loader 配置,可能切断 SSR 侧的 class 名提取链
最易被忽略的是:你以为在用 CSS Modules,其实服务端根本没“编译”它——它只是个普通对象字面量。真正起作用的 class 名,永远只在客户端 JS 执行那一刻才诞生。要让 SSR 不闪,就得让服务端也走一遍同样的 class 名生成逻辑,且确保两次输出完全一致。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











