前端社区里有一个很有趣的现象:样式方案的讨论热度,几乎每隔两三年就会换一个主角。前几年大家还在争论 CSS-in-JS 到底是不是未来,最近风向又迅速转向 Tailwind 的 utility-first 范式,而一些老牌项目则始终守着 CSS Modules 不动。每个方案都有一批坚定支持者,也都有从生产环境里踩过坑之后转身离开的人。

很多团队会遇到一个很实际的问题:新项目启动时,样式方案到底选什么?这个决定看似不大,但后期几乎无法低成本替换。因为样式方案会渗透到组件的写法、开发习惯、构建配置、主题机制,甚至团队协作的方式。我见过不少项目,业务逻辑重构得还算顺利,唯独样式层拆不动,最后只能整体推倒重来。
这篇文章不打算站队。我想从工程实践的角度,把 CSS-in-JS、Tailwind 和 CSS Modules 这三条技术路线的演进逻辑、适用边界和真实代价讲清楚。希望你看完之后,能根据自己的项目情况做出判断,而不是被社区舆论带着走。
样式方案到底在解决什么问题
要理解样式方案为什么一直在变,先得回到一个原点问题:原生 CSS 到底有什么让人无法忍受的地方。
最核心的痛点是全局作用域。CSS 里的每个选择器都是全局的,这意味着组件之间只要类名撞了,样式就会互相污染。大项目里最常见的处理方式就是约定命名规范,比如 BEM。但 BEM 只能靠人自觉约束,一旦团队扩张或者项目周期变长,类名冲突和样式覆盖的问题总会回来。
第二个痛点是死代码清理。CSS 文件里躺着大量已经没人引用的类名,删的时候怕误伤,不删又持续增加维护成本。第三个痛点是动态样式。当样式需要根据状态、主题或者用户偏好变化时,原生 CSS 的表达能力非常有限,变量和函数都难以覆盖复杂交互。
这三个问题,就是 CSS-in-JS、Tailwind、CSS Modules 各自试图解决的。它们没有谁全面胜出,只是在不同维度上做了取舍。
CSS-in-JS:用运行时换开发体验
CSS-in-JS 的代表是 styled-components 和 Emotion。它的核心思路很直接:把样式写在组件文件里,利用 JavaScript 的作用域天然隔离样式,不再担心类名冲突。
2017 到 2020 年这段时间,CSS-in-JS 几乎是 React 社区的事实标准。原因很好理解:它让开发者可以用模板字符串写样式,可以读取 props 做动态样式,可以自动处理前缀,而且组件卸载后样式自动清理,不会产生死代码。开发体验确实爽,尤其是写复杂交互组件的时候。
但它的代价也很明显。最直接的问题出在运行时开销上。比如 styled-components,它会在运行时解析样式字符串,插入 style 标签,并在 props 变化时重新计算样式。一个页面如果组件数量上去了,这一层的开销会被放大。有人测过,styled-components 在低端移动设备上对首屏渲染的影响非常可观。
// 一个典型的 styled-components 组件
const Button = styled.button`
padding: 12px 24px;
background: ${props => props.primary ? '#2563eb' : '#e2e8f0'};
color: ${props => props.primary ? '#fff' : '#1e293b'};
border-radius: 6px;
&:hover {
opacity: 0.8;
}
`;
再有一个问题是调试体验。虽然 DevTools 里能看到组件名,但样式来自哪个文件、哪个类名对应的规则是什么,排查起来比普通 CSS 费劲不少。尤其是在 SSR 场景下,CSS-in-JS 需要额外的配置来抽取样式,服务端和客户端的样式不一致时,会出现首屏闪烁。
不过话说回来,CSS-in-JS 并没有很多人说的那么不堪。它对小团队、快速迭代、组件库开发来说,效率优势非常明显。只是当你做一个大型 To B 系统,页面多、组件多、低端设备用户占比高的时候,运行时开销就成了一个绕不过去的坎。
CSS-in-JS 的典型误区
很多人会误以为 CSS-in-JS 是“零成本”的,因为写起来太顺手了。实际上,它的成本集中在运行时的样式计算和注入上。这个问题在首屏和长列表页面尤其明显。如果你选 CSS-in-JS,最好对样式计算耗时做一次性能预算,而不是等到用户反馈卡顿再排查。
Tailwind CSS:用约束换取一致性
Tailwind 的走红,本质上是对 CSS-in-JS 的一种反弹。它把样式方案的粒度从“组件”降到了“原子类”,所有样式都通过组合工具类来实现。
这套思路和 Bootstrap 那种预置组件完全不同。Bootstrap 给你一套写好的按钮和卡片,而 Tailwind 给你一堆砖块,让你自己拼。它的核心价值不是省代码,而是建立一套视觉约束。间距、颜色、字体大小都被固定在了设计令牌里,开发者很难写出脱离设计体系的样式。
<!-- 同样的按钮,Tailwind 写法 -->
<button class="px-6 py-3 rounded-md bg-blue-600 text-white hover:opacity-80">
Click me
</button>
从工程角度看,Tailwind 确实解决了几个老问题。因为所有类名都是预定义的,构建时可以扫描源码提取用到的类,生成最小的 CSS 文件,死代码问题被根治了。而且它是编译期生成 CSS,没有运行时,性能上比 CSS-in-JS 干净得多。
但 Tailwind 的代价也经常被低估。最大的争议是可读性和可维护性。一个复杂的组件,className 里可能堆了几十个类名,阅读和理解的成本非常高。虽然有 @apply 可以提取复用,但用多了又会回到 CSS 的老路上去,类名嵌套、覆盖、优先级问题全都回来了。
另一个问题是与组件抽象结合的要求。Tailwind 脱离组件框架很难独立工作。如果你用的是 React 或 Vue,可以封装成 Button、Card 这样的组件,把细节藏起来。但如果你的项目是后端模板直出,或者团队里没有组件化开发习惯,Tailwind 写起来会非常痛苦。
Tailwind 适合什么场景
说实话,Tailwind 最适合的是组件化程度高、设计系统相对完善的团队。因为它要求所有样式都通过类名组合完成,如果设计系统没有提前定义好色彩和间距,Tailwind 默认的设计令牌会让界面变得很“模板化”。另外,如果你做的是面向用户的营销页面、活动页这种视觉自由度高的页面,Tailwind 反而会显得碍手碍脚。
CSS Modules:最接近 CSS 本质的答案
CSS Modules 这几年被讨论得少,但它其实是很多大型项目坚持在用的方案。它的思路很简单:在构建阶段给类名加上一个哈希后缀,把全局 CSS 变成局部作用域。
/* Button.module.css */
.button {
padding: 12px 24px;
border-radius: 6px;
background: #2563eb;
color: white;
}
/* React 组件 */
import styles from './Button.module.css';
function Button({ primary }) {
return <button className={styles.button}>Click me</button>
}
CSS Modules 的好处非常实在:它没有运行时,所有样式在构建时就被确定了;它是纯 CSS,开发者可以完全复用已有的 CSS 技能;它保留了 CSS 的全部能力,媒体查询、动画、嵌套语法都能直接用;调试时 DevTools 里显示的是加了哈希的类名,配合 sourceMap 也能定位到源文件。
更关键的是,CSS Modules 的迁移成本很低。你可以把传统 CSS 文件直接改成 .module.css,类名冲突问题立刻解决,不需要改组件结构。
但它也不是没有短板。CSS Modules 处理动态样式很笨拙,通常只能通过 CSS 变量或者条件切换类名来实现,写多了确实不优雅。另外它不提供任何设计约束,设计规范靠团队自觉,这也是很多人转向 Tailwind 的原因。
三种方案放在一起看
把三种方案放在同一张表里,差异会看得更清楚。
| 维度 | CSS-in-JS | Tailwind CSS | CSS Modules |
|---|---|---|---|
| 作用域隔离 | 通过 JS 作用域隔离 | 无隔离,靠原子类约定 | 构建期哈希隔离 |
| 运行时开销 | 有,样式解析和注入 | 无,编译期生成 | 无,纯构建期处理 |
| 动态样式 | 强,props 直接驱动 | 弱,需条件类名 | 弱,需 CSS 变量配合 |
| 设计约束 | 弱,依赖开发规范 | 强,设计令牌内置 | 弱,依赖开发规范 |
| 调试体验 | 一般,样式来源难追踪 | 一般,类名可读性差 | 好,接近原生 CSS |
| 学习成本 | 中,需理解运行时机制 | 中,需记忆类名体系 | 低,纯 CSS 技能 |
| 适合场景 | 小团队、组件库、快速迭代 | 组件化成熟、设计系统完善 | 大型项目、长期维护、SSR |
这里要说明一点:这张表是典型的工程取舍,不是优劣排序。一个方案在某项上是短板,往往意味着它在另一个维度上有不可替代的优势。比如 CSS-in-JS 的运行时开销是缺点,但它的动态样式能力确实最强,适合做主题切换。
常见误区:三种方案不是非此即彼
很多讨论把三种方案对立起来,好像选了一个就必然排斥另外两个。但真实项目里,它们经常是共存的。
一个很常见的组合是:全局样式和布局用 CSS Modules,交互密集的组件用 CSS-in-JS,设计令牌和工具类用 Tailwind。这种混用听起来不够“纯粹”,但实际效果往往不错。因为每种方案都有自己的舒适区,强制统一反而会增加成本。
另一个误区是认为 Tailwind 能取代 CSS Modules。它们的抽象层次根本不同。CSS Modules 解决的是作用域隔离,Tailwind 解决的是样式原子化。如果你用 Tailwind,同时仍然需要处理第三方组件库的样式覆盖,CSS Modules 反而可以派上用场。
还有一种误区是迷信“零运行时”方案。很多团队因为性能选 Tailwind,结果发现项目里封装了一堆复杂组件,类名组合的可维护性差到不行,最后又改回 CSS Modules。性能不是唯一指标,开发效率和可维护性同样重要。
到底怎么选:几个实际判断标准
抛开社区舆论,我建议从下面几个维度来评估你的项目更适合哪种方案。
- 团队规模与流动率:团队大、人员流动频繁,选 CSS Modules 最稳妥,因为它是纯 CSS,任何人都能快速上手;团队小而精,选 CSS-in-JS 或 Tailwind 都能发挥效率优势。
- 项目生命周期:长期维护的企业系统,优先考虑 CSS Modules 或 Tailwind,尽量避免运行时方案;短平快的活动页或中后台原型,CSS-in-JS 的开发效率最划算。
- 对性能的敏感度:如果系统面向低端设备、弱网环境,避免使用 CSS-in-JS,Tailwind 的编译期生成 CSS 是更安全的选择。
- 设计规范成熟度:设计系统已经定义好色彩、字号、间距,Tailwind 的 utility 类能帮你严格执行;如果设计还没定型,CSS Modules 给了你更多自由调整空间。
如果你的团队正在从零搭建一个中大型前端项目,我比较推荐一种渐进式组合:以 CSS Modules 作为基础样式方案,保证可维护性和性能;在需要复杂动态样式的局部场景引入 CSS-in-JS;如果设计系统完善,可以逐步引入 Tailwind 的 utility 类来规范间距和色彩。
这种组合方式不是最“潮”的,但它在可维护性、开发效率、性能之间找到了一个相对平衡的点。而且每层方案之间的边界清晰,未来即使要调整,也不会伤筋动骨。
最后说点实际的
样式方案没有银弹,每一次演进本质上都是对之前方案缺陷的修正,同时也会带来新的问题。CSS-in-JS 解决了作用域和动态样式,但引入了运行时成本;Tailwind 解决了设计约束和死代码,但牺牲了可读性;CSS Modules 最稳妥,但动态表达能力有限。
我的建议是:不要因为某个方案在社区里很火就选它,也不要因为某个方案“过时”就急于抛弃。先想清楚你的项目会遇到什么问题,再看哪种方案的取舍你能接受。样式方案和其他技术选型一样,没有最好,只有最合适。
如果你正在这个岔路口犹豫,不妨先做一个最小化的技术验证:用你想选的方案写一个包含动态交互、响应式布局、主题切换的页面,感受一下它在真实开发中的节奏,再决定也不迟。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/463/