唯一100%可控方案是将首屏必需css内联至最顶部的标签,体积≤10kb、不加media属性;服务端需用同一cache实例提取样式并注入html,客户端hydration前必须复用该实例,且须带正确data-属性以确保ssr识别。

SSR 中的闪屏(FOUC)不是配置没选对,而是关键样式没在 HTML 首次绘制前就位——必须让首屏所需 CSS 文本出现在 顶部的 <style></style> 标签里,且体积 ≤10KB。
为什么放在
浏览器只对“匹配当前环境 + 无 JS 干预 + 正确 Content-Type”的 <link rel="stylesheet"> 做阻塞渲染。以下情况会直接绕过阻塞逻辑:
-
media值不匹配(如media="(min-width: 768px)"在手机上)→ 浏览器当它“暂时不用”,异步加载 - CSS 文件里含
@import→ 强制串行请求,首屏样式链断裂 -
<link>是 JS 动态插入的(比如document.head.appendChild())→ HTML 预加载器根本看不到它 - 服务端返回了
Content-Type: text/plain而非text/css→ 浏览器拒绝解析,降级为非阻塞
必须内联 critical CSS,且满足三个硬约束
内联不是把整个 main.css 复制进去,而是提取首屏 DOM 真正用到的最小样式集合(如 .header、.hero、基础重置),并严格满足:
- 必须用
<style></style>,不能是<link rel="stylesheet"> - 体积 ≤10KB(移动端保守值),否则可能阻塞 HTML 解析本身
- 不能加
media属性(如media="(prefers-color-scheme: dark)"),否则浏览器跳过阻塞 - 必须放在
最顶部,早于任何<script></script>或<link>
别手动维护——用 critters(Vite 插件)、critical(CLI)或 Next.js 内置工具自动生成,否则极易过期。
CSS-in-JS 库(Emotion / Ant Design)的 SSR 闪屏根因是 cache 不一致
服务端生成的 class 名和客户端 hydration 时生成的不一致,导致 React 拒绝复用 DOM 节点,强制重渲染。核心破局点只有一个:cache 实例必须完全复用。
- Next.js App Router 中,
RootStyleRegistry必须用useState(() => createCache())初始化,不能每次请求都新建 - 服务端调用
extractStyle(cache)后,把结果字符串写进 HTML 的;客户端 hydration 前,必须用同一个cache实例初始化<styleprovider></styleprovider> - 所有
<style></style>标签必须带正确data-属性:data-emotion="css"(Emotion)或data-ant-css(Ant Design),否则 SSR 提取工具识别不到,样式只在客户端注入 - PostCSS、Emotion、Vite 的 CSS 缓存不会自动失效,改完配置后务必手动清空
.next/cache/postcss和node_modules/.vite/deps
Tailwind 的闪屏往往藏在 PostCSS 配置和 content 扫描路径里
服务端吐出的 HTML 没有对应类名规则,是因为构建时根本没扫到那些文件,或者插件顺序错了。
- 检查
postcss.config.js中插件顺序是否为:{ 'tailwindcss': {}, 'autoprefixer': {} },颠倒会导致@apply、hover:类被截断 -
content字段必须显式覆盖所有动态组件路径,例如:app/**/*.{ts,tsx}、src/components/**/*.{ts,tsx},连dynamic(() => import('./Chart'))的路径也得列进去 - 删掉
.next/cache/postcss/目录再构建,否则旧缓存会掩盖配置变更 - CDN 或 JS 驱动方案(如 UnoCSS)必须加 loading 类兜底:
body { visibility: hidden; },JS 加载完成再移除,且设 3 秒超时防卡死
最容易被忽略的是多个问题共存:PostCSS 顺序错、content 漏扫、JS 注入延迟,现象都是“闪一下”,但修复必须逐项排除,不能靠猜。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











