
一句话:主流方案有四种——CSS Modules(无运行时、作用域隔离)、Tailwind(原子化、无运行时)、CSS-in-JS(有运行时开销)、普通 CSS/Sass(需自己管命名);新项目的主流选择是「CSS Modules 或 Tailwind」,CSS-in-JS 因运行时开销在 React 18 并发下逐渐退潮。
一、四种方案对比
| 维度 | CSS Modules | Tailwind | CSS-in-JS(styled-components/emotion) | 普通 CSS/Sass |
|---|---|---|---|---|
| 运行时开销 | 无 | 无 | 有(运行时生成样式) | 无 |
| 作用域隔离 | ✅(编译时加 hash) | ✅(原子类) | ✅ | ❌(需 BEM/命名约定) |
| 动态样式 | ⚠️(需 CSS 变量) | ⚠️(需条件类名) | ✅(直接写 JS 表达式) | ⚠️ |
| 类型安全(TS) | ⚠️(需生成 d.ts) | ✅(插件给类型) | ✅ | ❌ |
| 产出体积 | 小(按需) | 小(PurgeCSS) | 大(运行时 + 样式) | 小 |
| 学习成本 | 低 | 中(要记类名) | 低 | 低 |
| SSR 支持 | ✅ | ✅ | ⚠️(需额外配置,避免 FOUC) | ✅ |
| 调试体验 | 一般(类名是 hash) | 一般(类名很长) | 好(类名可读) | 好 |
| 设计系统契合 | 中 | 好(token 化) | 好 | 中 |
| 现状 | 主流 | 主流 | ⚠️ 退潮(运行时开销) | 老项目 |
⭐ 一句话选型:
① 团队有设计系统 / 喜欢原子化 → Tailwind
② 想要「普通 CSS 的写法 + 作用域隔离」 → CSS Modules
③ 需要「样式与逻辑强耦合、动态样式极多」 → CSS-in-JS(但要接受代价)
④ 老项目 / 简单页面 → 普通 CSS + BEM
二、CSS Modules
/* Button.module.css */
.button {
padding: 8px 16px;
border-radius: 6px;
border: 1px solid transparent;
}
.primary {
background: #2563eb;
color: white;
}
/* 组合(复用其它类) */
.danger {
composes: button;
background: #dc2626;
}
// Button.tsx
import styles from './Button.module.css';
import clsx from 'clsx';
interface Props {
variant?: 'primary' | 'danger';
className?: string;
}
export function Button({ variant = 'primary', className, ...rest }: Props & React.ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
className={clsx(styles.button, styles[variant], className)} // ⭐ 合并外部 className
{...rest}
/>
);
}
/* ✅ 与 CSS 变量结合做「动态样式」 */
.button {
background: var(--btn-bg, #2563eb);
color: var(--btn-fg, white);
}
// 通过 CSS 变量传动态值(避免「为每个值生成一个类」)
<button style={{ '--btn-bg': color } as React.CSSProperties} className={styles.button} />;
⭐ CSS Modules 的三个要点:
① 「编译时」就把类名变成「唯一 hash」→ 天然作用域隔离
② 支持 `composes` 复用(比 Sass 的 @extend 更可控)
③ 动态样式用「CSS 变量 + 内联 style」而不是「动态生成类名」
⚠️ TS 支持:需要类型声明(否则 `styles.button` 报错)
// src/types/css-modules.d.ts(或用「vite-plugin-sass-dts」自动生成)
declare module '*.module.css' {
const classes: Record<string, string>;
export default classes;
}
三、Tailwind
// ✅ 原子类直接写在 className 里
export function Button({ variant = 'primary', ...rest }: ButtonProps) {
return (
<button
className={clsx(
'inline-flex items-center rounded-md px-4 py-2 text-sm font-medium transition',
variant === 'primary' && 'bg-blue-600 text-white hover:bg-blue-700',
variant === 'danger' && 'bg-red-600 text-white hover:bg-red-700',
)}
{...rest}
/>
);
}
// tailwind.config.js —— 把「设计 token」写进主题(避免魔法值)
export default {
content: ['./index.html', './src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
},
spacing: { 18: '4.5rem' },
borderRadius: { xl: '0.75rem' },
},
},
plugins: [],
};
⭐ Tailwind 的取舍:
✅ 优点:
· 无运行时(编译时生成 CSS)
· 产出小(只保留用到的类)
· 「设计约束」——间距/颜色都来自主题(不易出现魔法值)
· 改样式不用「起名」(也就不用纠结命名)
· TS 插件能提示类名
❌ 缺点:
· className 很长(可读性差)
· 「多条件样式」需要 clsx/cva 之类的工具
· 团队需要统一约定(否则会「各写各的」)
· 学习成本(要记住常用类名)
✅ 缓解方式:
· 用 `cva`(class-variance-authority)管理「变体」 → 见下
· 用 `@apply` 抽取「重复的组合」(但要克制)
// ✅ 用 cva 管理「变体」(Tailwind 项目的常见实践)
import { cva, type VariantProps } from 'class-variance-authority';
const button = cva('inline-flex items-center rounded-md font-medium transition', {
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
danger: 'bg-red-600 text-white hover:bg-red-700',
ghost: 'bg-transparent hover:bg-gray-100',
},
size: {
sm: 'px-2 py-1 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
},
},
defaultVariants: { variant: 'primary', size: 'md' },
});
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof button> {}
export function Button({ variant, size, className, ...rest }: ButtonProps) {
return <button className={button({ variant, size, className })} {...rest} />;
}
四、CSS-in-JS 的原理与代价
// styled-components / emotion 的写法
import styled from '@emotion/styled';
const Button = styled.button<{ $variant: 'primary' | 'danger' }>`
padding: 8px 16px;
border-radius: 6px;
background: ${(p) => (p.$variant === 'primary' ? '#2563eb' : '#dc2626')};
color: white;
`;
// 使用
<Button $variant="primary">确定</Button>;
⭐ CSS-in-JS 的价值(为什么曾经流行):
① 样式与组件「同文件」(就近维护)
② 动态样式「直接写 JS 表达式」(不需要类和 CSS 变量的间接)
③ 类名「可读」(便于调试)
④ 天然作用域隔离
⚠️ 但它的三类代价(为什么现在退潮):
① 运行时开销
· 每次渲染都要「序列化样式 + 生成/插入 <style>」
· 样式的「hash 计算」也在运行时
→ 首屏与大列表场景有明显成本
② React 18 并发下的问题
· 渲染可中断 → 样式插入的时机需要专门处理
(这就是 React 18 引入 useInsertionEffect 的原因——给 CSS-in-JS 库用)
③ SSR 的复杂度
· 需要「收集样式 → 注入 HTML」→ 避免 FOUC(样式闪烁)
· 需要额外的配置与 API(如 emotion 的 extractCritical)
// ✅ 如果仍要用 CSS-in-JS,React 18 要求库用 useInsertionEffect
// (库作者的事,但你要知道「为什么有 useInsertionEffect 这个 Hook」)
function useCSS(rule: string) {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => style.remove();
}, [rule]);
}
⭐ 「零运行时」的 CSS-in-JS(现代替代):
① 编译时提取——如 vanilla-extract、Linaria、Panda CSS
→ 写法仍是「样式与逻辑同文件」,但构建时产出静态 CSS(无运行时)
② 这是「CSS-in-JS 的体验 + 静态 CSS 的性能」的折中
// vanilla-extract 示例(编译时产出 CSS)
// button.css.ts
import { style, styleVariants } from '@vanilla-extract/css';
export const base = style({ padding: '8px 16px', borderRadius: 6 });
export const variant = styleVariants({
primary: [{ background: '#2563eb', color: 'white' }],
danger: [{ background: '#dc2626', color: 'white' }],
});
五、选型建议
决策树:
① 团队已有「设计系统 + token」?
├─ 是 → Tailwind(token 直接映射到主题)或 CSS Modules + CSS 变量
└─ 否 → 继续
② 需要「极致的动态样式」(如「颜色由用户输入决定且数量无限」)?
├─ 是 → CSS-in-JS(或「零运行时」的 vanilla-extract)
└─ 否 → 继续
③ 团队偏好「原子化」还是「语义化」?
├─ 原子化 → Tailwind
└─ 语义化 → CSS Modules(或 Sass)
④ 有 SSR?
→ 都支持;CSS-in-JS 需要额外配置(避免 FOUC)
⭐ 经验:
· 新项目大多数场景 → Tailwind 或 CSS Modules(都是「无运行时」)
· 组件库 → CSS Modules / vanilla-extract / 自带设计 token
· 老项目/简单页面 → 保持原样(普通 CSS + BEM)
· 避免「同一项目里混用三四种方案」
⚠️ 「混用」的常见做法(合理):
· Tailwind 做「工具类与布局」 + CSS Modules 做「复杂组件样式」
· 全局「设计 token」用 CSS 变量(`:root { --color-brand: ... }`)
· 主题切换用 CSS 变量 + data-attribute(无运行时)
/* ✅ 用 CSS 变量做主题(运行时切换无需重新生成样式) */
:root {
--color-bg: #ffffff;
--color-fg: #111827;
--color-brand: #2563eb;
}
[data-theme='dark'] {
--color-bg: #0f172a;
--color-fg: #e5e7eb;
--color-brand: #60a5fa;
}
.card {
background: var(--color-bg);
color: var(--color-fg);
}
// 切换主题只需改一个属性(甚至只用一个 <script> 就能在「首屏前」设置)
function toggleTheme() {
const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);
}
六、其它常见问题
① Sass/Less 还需要吗?
· 现代 CSS 已有「嵌套、变量、:is/:where」(Sass 的部分能力已被原生吸收)
· Sass 的价值主要剩「mixin / function / @use 模块化」
· 新项目可以只用原生 CSS + PostCSS(autoprefixer / postcss-preset-env)
② PostCSS 做什么?
· autoprefixer(自动加浏览器前缀)
· postcss-preset-env(把「未来的 CSS 语法」编译成兼容语法)
· Tailwind 本身就是一个 PostCSS 插件
③ 「样式冲突」怎么排查?
· 用 DevTools 看「应用的规则来源」
· 优先用「作用域方案」(Modules/Tailwind)从源头避免
· 检查「层级/权重」与「加载顺序」
④ 「样式被打包重复」怎么办?
· 检查是否「多处引入同一个样式文件」
· Tailwind 的 content 配置要正确(否则扫描不到类名 → 样式丢失)
七、常见坑
// ① 用 Tailwind 但 content 配置不全 → 生产环境「样式丢失」
content: ['./src/**/*.{ts,tsx}']; // ⚠️ 漏了某些文件类型/目录
// ② Tailwind 用「动态拼接类名」→ 扫描不到 → 样式丢失
className={`text-${color}-500`} // ❌ 无效
// ✅ 用完整的类名映射
const colors = { red: 'text-red-500', blue: 'text-blue-500' };
className={colors[color]} // ✅
// ③ CSS Modules 里用「全局类名」却忘了 :global
:global(.ant-btn) { } // ✅
.ant-btn { } // ❌ 被 hash 化(选择不到)
// ④ CSS Modules 的类名「动态拼」导致 TS 报错/取不到
styles[`btn-${variant}`] // ⚠️ 类型不友好
// ✅ 用「显式映射」或 camelCase 命名
// ⑤ CSS-in-JS 在「大列表」里每行都生成样式(运行时开销爆炸)
// ⑥ CSS-in-JS 的 SSR 配置不全 → FOUC(样式闪烁)
// ⑦ 混用多种方案导致「权重/顺序」难以预测
// ⑧ 用「内联 style」写「大量静态样式」(失去缓存与复用)
<div style={{ padding: 16, color: 'red', fontSize: 14, ... }} /> // ⚠️
// ⑨ 忘记「优先级 / 层叠」问题(!important 满天飞)
// ⑩ 用「样式隔离方案」但仍有「全局 reset 冲突」
// ⑪ 主题切换用「重新生成样式」(应该有 CSS 变量 + data-attribute)
// ⑫ 忘记「图片/字体等资源的路径」(CSS 里的 url() 要能被构建处理)
// ⑬ Tailwind 的「任意值语法」用太多(失去设计约束的意义)
className="w-[317px] text-[#a1b2c3]" // ⚠️ 偶尔可以,滥用则失控
// ⑭ 用「100vh」而不用「100dvh」(移动端地址栏导致高度跳变)
// ⑮ 忽略「CSS 体积监控」(未使用的样式随版本累积)
// ② 的正确写法
// ❌
<div className={`bg-${tone}-100 text-${tone}-700`} />
// ✅ 用「完整的类名常量」
const toneClasses = {
success: 'bg-green-100 text-green-700',
warning: 'bg-amber-100 text-amber-700',
error: 'bg-red-100 text-red-700',
} as const;
<div className={toneClasses[tone]} />
八、面试延伸
- 「React 项目有哪些样式方案?怎么选?」
四种:① CSS Modules——无运行时、编译时作用域隔离、写法就是普通 CSS(主流);② Tailwind——原子化、无运行时、产出小、天然与设计 token 契合(主流);③ CSS-in-JS(styled-components/emotion)——样式与逻辑同文件、动态样式直接写 JS,但有运行时开销 + SSR 复杂度(近年退潮);④ 普通 CSS/Sass——需自己管命名(老项目)。新项目推荐前两种,关键判断是「是否有设计系统」与「是否需要极致动态样式」。 -
「CSS-in-JS 有什么代价?」
三类:① 运行时开销——每次渲染都要「序列化样式、计算 hash、生成/插入<style>」(首屏与大列表场景明显);② React 18 并发下的问题——渲染可中断,样式插入时机需要专门处理(这也是 React 18 引入useInsertionEffect的原因,它是专门给 CSS-in-JS 库用的 Hook);③ SSR 复杂——需要「收集样式并注入 HTML」以避免 FOUC,配置与 API 更繁琐。所以现在出现了「零运行时」的编译时方案(vanilla-extract、Linaria、Panda CSS)。 -
「Tailwind 有什么优缺点?」
优点:① 无运行时(编译时生成 CSS);② 产出小(只保留用到的类);③ 设计约束(间距/颜色都来自主题,避免魔法值);④ 不用起名(省去命名成本)。缺点:① className 很长(可读性差);② 多条件样式需要clsx/cva之类工具;③ 需要团队统一约定;④ 有学习成本。关键坑是「动态拼接类名」——Tailwind 靠静态扫描,text-${color}-500这类拼接会被漏掉(必须用完整的类名映射)。 -
「CSS Modules 怎么处理动态样式?」
不推荐「动态生成类名」(会让类型与构建都变复杂)。推荐两条路:① CSS 变量 + 内联 style——在 CSS 里写background: var(--btn-bg),在 JSX 里用style={{ '--btn-bg': color }}传值(类型上需要断言为 CSSProperties);② 条件类名——用clsx在两个预定义的类之间切换(适合「有限个变体」)。核心原则是「类名保持静态、动态值走 CSS 变量」。 -
「主题切换怎么做?」
最佳实践是「CSS 变量 +data-*属性」:① 在:root定义「浅色」的变量,在[data-theme='dark']覆盖为「深色」;② 切换主题时只改document.documentElement.dataset.theme(让 CSS 自动生效,不需要重新生成或加载样式);③ 持久化到localStorage;④ 避免首屏闪烁(FOUC)——在 HTML 里内联一段<script>,在「页面渲染前」就根据localStorage设置data-theme(或由服务端从 cookie 读出并注入class,后者更适合 SSR)。 -
「为什么会有 FOUC?怎么避免?」
FOUC(Flash of Unstyled Content)指「页面先以无样式状态显示,然后样式才生效」——表现为「闪一下」。两种典型原因:① CSS-in-JS 的 SSR——服务端渲染的 HTML 里没有对应样式,客户端 JS 执行后才插入<style>;② 主题由客户端脚本设置——首屏渲染时用的默认主题,JS 执行后才切成用户主题。解法:① CSS-in-JS 要配置「样式提取并注入 HTML」(如 emotion 的extractCritical);② 主题在「首屏渲染前」确定(内联脚本或服务端读 cookie 注入)。 -
「样式冲突怎么排查/避免?」
避免:① 用「作用域方案」(CSS Modules 的编译时 hash、Tailwind 的原子类、CSS-in-JS 的运行时隔离)从源头避免重名;② 全局样式只放「reset、CSS 变量、极少数工具类」;③ 组件样式不写在「全局选择器」里(如ul li这类会污染全局)。排查:① DevTools 的 Styles 面板会显示「哪些规则命中了这个元素、来自哪个文件、有没有被覆盖」;② 检查「加载顺序」与「选择器权重」;③ 如果是第三方组件库的样式被覆盖,用「提高权重」或「作用域包裹」(但优先考虑「官方定制的 API」,如 antd 的 ConfigProvider)。 -
「CSS 体积怎么优化?」
四个方向:① 避免未使用的样式——Tailwind(PurgeCSS 只保留用到的类)、CSS Modules(按需引入);② 避免重复——检查多个入口是否重复引入同一份样式、组件库是否整包引入(改成按需);③ 减少「运行时代价」——CSS-in-JS 换「零运行时」(vanilla-extract);④ 压缩与拆分——生产压缩(cssnano)、关键 CSS 内联(首屏)、其余异步加载。此外要注意「不要用@import串行引入」(会阻塞),改用构建工具的合并。
一句话速记
四种方案:CSS Modules(无运行时、编译时 hash 隔离、写普通 CSS,动态值走 CSS 变量)、Tailwind(无运行时、原子类、与设计 token 契合,坑是「动态拼类名导致样式丢失」,用
cva管变体)、CSS-in-JS(样式与逻辑同文件但有运行时开销 + SSR 复杂 + 并发下需要useInsertionEffect,已被「零运行时」的 vanilla-extract 类方案替代)、普通 CSS/Sass(需自己管命名);选型看「有没有设计系统 + 是否需要极致动态样式」;主题切换用「CSS 变量 +data-theme属性」(零运行时、无 FOUC,配合内联脚本或服务端读 cookie);FOUC 的两个来源是「CSS-in-JS 的 SSR 未提取样式」与「主题由客户端脚本设置」;避免样式冲突的根本办法是「作用域方案 + 全局只放 reset 与变量」。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!