从 CSS-in-JS 到 Tailwind 再到 CSS Modules:前端样式方案到底该怎么选

本文梳理 CSS-in-JS、Tailwind CSS 与 CSS Modules 三种主流前端样式方案的演进逻辑与适用边界,结合实际工程场景分析各自优势、代价与常见误区,并给出不同团队规模下的选型建议。

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

AI technology illustration

很多团队会遇到一个很实际的问题:新项目启动时,样式方案到底选什么?这个决定看似不大,但后期几乎无法低成本替换。因为样式方案会渗透到组件的写法、开发习惯、构建配置、主题机制,甚至团队协作的方式。我见过不少项目,业务逻辑重构得还算顺利,唯独样式层拆不动,最后只能整体推倒重来。

这篇文章不打算站队。我想从工程实践的角度,把 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/

(0)
上一篇 1小时前
下一篇 1小时前

相关推荐