fouc 是因服务端未内联关键 css 导致浏览器先渲染无样式 html,再重绘;需在 最前内联 css、确保 css modules/tailwind 类名服务端客户端一致,并延迟 hydration 至样式解析完成。

为什么直接 import CSS 会闪
React SSR 项目里,import './Button.module.css' 或 import 'bootstrap/dist/css/bootstrap.min.css' 在服务端不执行样式注入,浏览器先渲染无样式的 HTML,等 JS 加载、CSS 下载解析后才重绘——这就是 FOUC(Flash of Unstyled Content)。它不是报错,是肉眼可见的“白→跳→正”过程。
关键 CSS 必须内联到 最前面
外链 CSS 至少多一个 RTT 延迟(300–800ms),而内联能确保样式随 HTML 一起到达。但不能手动复制整段 CSS(bootstrap.min.css gzip 后约 200KB,会阻塞 HTML 解析)。
- Next.js App Router:在
app/layout.tsx中用?raw加载:import bootstrapCss from 'bootstrap/dist/css/bootstrap.min.css?raw',然后写<style>{bootstrapCss}</style>,且必须放在内最前(早于<title></title>) - Razzle / 自建 Express SSR:在服务端生成 HTML 字符串时,把提取出的关键 CSS 字符串拼进
<style>...</style> - 不要对
bootstrap-icons或字体文件这么做——它们不参与 FOUC,走<link rel="stylesheet">即可
Tailwind 或 CSS Modules 的类名必须静态可扫描
Tailwind JIT 模式只认源码中写死的类名字面量;CSS Modules 在 SSR 中闪烁,根本原因是服务端与客户端生成的哈希类名不一致(如 Button__primary___abc123 vs Button__primary___def456)。
- Tailwind:禁用模板字符串拼接,改用静态条件 ——
className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'},并确认tailwind.config.ts的content字段包含所有 JSX/TSX 路径(如["./src/**/*.{ts,tsx}"]) - CSS Modules:在 Vite 或 Webpack 配置中显式固定
generateScopedName: '[name]__[local]___[hash:base64:5]',禁用css.modules.symbols,且服务端与客户端构建必须使用相同配置 - 若用
css.lightningcss,同步设置css.lightningcss.cssModules.scopeBehaviour: 'local',否则 Lightning CSS 会绕过默认逻辑
客户端 hydration 前必须等样式就绪
即使 CSS 已内联或预加载,如果 React hydrateRoot 执行得太早(比如在 DOMContentLoaded 里触发),而 document.styleSheets 还没解析完规则,依然会闪。
- 不要在
DOMContentLoaded立即 hydrate;应轮询document.styleSheets[0].cssRules?.length或监听load事件 - 更稳妥的做法:给关键
<link rel="stylesheet">加onload回填逻辑,并在 onload 触发 hydrate - Next.js Pages Router 可用
_document.tsx注入样式;App Router 则需放弃 ServerStyleSheet,改用@emotion/server或静态提取方案
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











