CSS主题变量在SSR中闪烁,是因为服务端无document无法读取用户偏好,只能按默认主题(如light)渲染,客户端水合后才切换主题导致视觉跳变;必须启用cssVariables、正确配置colorSchemeSelector(class或data)、确保InitColorSchemeScript在React渲染前执行于app/layout.tsx的<head>中,且CSS变量需定义在:root或<html>上。

为什么 CSS 主题变量在 SSR 中会闪烁
服务端渲染时,document 不存在,无法读取 localStorage 或 prefers-color-scheme,所以服务端只能按默认主题(通常是 light)生成 HTML 和内联样式;客户端水合后才读取用户偏好并切换主题,导致视觉跳变。这不是 CSS 加载慢,而是服务端和客户端“看到的初始主题”不一致。
必须启用 CSS 变量 + 正确的 colorSchemeSelector
Material UI 的 InitColorSchemeScript、next-themes 等方案都依赖 CSS 变量模式生效。如果主题里没开 cssVariables,或 colorSchemeSelector 值与脚本注入方式不匹配,所有后续配置都会失效。
-
colorSchemeSelector: 'class'→ 脚本往<html>上加class="dark",CSS 规则需写成.dark { --color-primary: #111; } -
colorSchemeSelector: 'data'→ 脚本设data-color-scheme="dark",规则得是[data-color-scheme="dark"] { --color-primary: #111; } - 别混用:比如主题设
class,但自己手写 JS 改data-theme,变量压根不会被应用
InitColorSchemeScript 必须在 React 渲染前执行
这个组件本质是一段内联 <script>,它要在 React.createRoot().render() 之前运行,否则就失去意义。Next.js App Router 下,必须放在 app/layout.tsx 的 <head> 内,且不能包裹在任何 React 组件中。
- 错误写法:
<Head><InitColorSchemeScript /></Head>(Head是旧 Pages Router 的 API) - 正确位置:在
app/layout.tsx的export default function RootLayout()内,直接写在<html>标签内部、<body>之前 - 若用了
next-themes,它已内置等效逻辑,无需再手动加InitColorSchemeScript,但必须用它的ThemesProvider包裹整个应用
localStorage 同步时机容易被忽略
用户手动切换主题后,localStorage 写入是同步的,但服务端下次渲染仍看不到——这没问题;真正危险的是客户端水合时读取了过期值(比如用户刚切 dark,但页面刷新后脚本读到的还是旧的 light)。所以初始化脚本必须明确指定 fallback 行为:
立即学习“前端免费学习笔记(深入)”;
- 用
defaultMode="system"时,脚本会查matchMedia('(prefers-color-scheme: dark)'),无需 localStorage - 用
defaultMode="light"且依赖用户选择时,必须确保storageKey与你代码里读写 localStorage 的 key 完全一致(比如都用'mui-color-scheme') - 避免在
useEffect里读 localStorage 后再调theme.palette.mode === 'dark'判断——SSR 阶段该判断永远为 false,会导致服务端/客户端不一致
最常漏掉的一点:CSS 变量必须作用于 :root 或 <html>,不能只写在某个组件的 scoped style 里;否则即使脚本正确挂了 class,变量也传不到子元素。


















