CSS Modules vs CSS-in-JS vs 原子化 CSS:样式方案终极横评
引言
前端 CSS 方案经历了至少三次革命:CSS Modules(2015,构建时作用域隔离)、CSS-in-JS(2014-2020,运行时样式注入)、原子化 CSS(2017-2025,Tailwind/UnoCSS 等)。每种方案都解决了一些问题,也带来新的问题。本文将深入对比这三大方案的原理、优缺点、性能影响、开发体验,并给出选型决策树。
一、CSS Modules:构建时作用域隔离
1.1 原理
CSS Modules 通过构建工具在编译时对 class 名做哈希重命名,实现样式作用域隔离。
/* Button.module.css */
.button { background: blue; color: white; }
.primary { composes: button; background: darkblue; }
import styles from './Button.module.css'
function Button({ primary }) {
return <button className={primary ? styles.primary : styles.button}>Click</button>
}
编译后:<button class="Button_primary__a1b2c">Click</button>
1.2 优点
- 零运行时开销:所有处理在构建时完成
- 原生 CSS 语法:学习成本接近零
- 作用域隔离:天然避免样式冲突
- 支持预处理器:可无缝使用 Sass/Less/Stylus
1.3 缺点
- 样式与组件分离:跨文件管理,组件内聚性降低
- 复用困难:跨组件复用需要 composes 或全局 class
- 动态样式受限:基于状态的样式切换需要多个 class
1.4 Vue 中使用
<template>
<button :class="[$style.button, { [$style.primary]: isPrimary }]">Click</button>
</template>
<style module>
.button { padding: 8px 16px; }
.primary { background: blue; }
</style>
二、CSS-in-JS:把样式写进 JS
2.1 运行时方案(styled-components / emotion)
import styled from '@emotion/styled'
const Button = styled.button<{ primary?: boolean }>`
background: ${props => props.primary ? 'blue' : 'gray'};
color: white;
padding: 8px 16px;
&:hover { background: ${props => props.primary ? 'darkblue' : 'darkgray'}; }
`
2.2 零运行时方案(vanilla-extract / linaria)
// button.css.ts (vanilla-extract)
import { style, styleVariants } from '@vanilla-extract/css'
export const button = style({ padding: '8px 16px', color: 'white' })
export const variant = styleVariants({
primary: { background: 'blue' },
secondary: { background: 'gray' },
})
2.3 优缺点
- ✅ 强类型 + 动态主题、样式与组件同文件、动态样式容易、死代码自动清理
- ❌ 运行时开销(仅运行时方案)、包体积大、SSR 复杂、DevTools 调试难
2.4 性能基准
| 指标 | CSS Modules | styled-components | vanilla-extract |
|---|---|---|---|
| 首次渲染耗时 | 50ms | 280ms | 55ms |
| 包体积(gzip) | ~3KB | ~12KB+ | ~3KB |
| SSR 注水大小 | 小 | 大 | 小 |
| DevTools 可读性 | 良好 | 差 | 良好 |
三、原子化 CSS:Tailwind / UnoCSS
3.1 原理
原子化 CSS 预先生成一组最小 CSS class,每个 class 只做一件事,开发者通过组合 class 表达样式。
<button className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Click</button>
| 方案 | 生成时机 | CSS 体积 |
|---|---|---|
| Tailwind 4 (CSS-first) | 构建时扫描 | 按需 ~10-30KB |
| UnoCSS | 按需扫描 + 即时生成 | 极致小 |
3.2 UnoCSS 的可扩展性
// uno.config.ts
export default defineConfig({
presets: [presetUno(), presetAttributify(), presetIcons()],
rules: [
['m-1', { margin: '0.25rem' }],
[/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
],
shortcuts: { 'btn': 'px-4 py-2 rounded' },
})
<!-- 属性化写法 -->
<button bg="blue-500 hover:blue-600" text="white" px="4" py="2" rounded>Click</button>
<i class="i-mdi-home text-2xl" />
四、核心能力对比
| 维度 | CSS Modules | CSS-in-JS (运行时) | CSS-in-JS (零运行时) | Tailwind | UnoCSS |
|---|---|---|---|---|---|
| 作用域隔离 | ✅ 自动 | ✅ 自动 | ✅ 自动 | ⚠️ 全局 | ⚠️ 全局 |
| 动态样式 | ⚠️ 有限 | ✅ 极强 | ✅ 中等 | ⚠️ 有限 | ⚠️ 有限 |
| 运行时开销 | ❌ 无 | ⚠️ 大 | ❌ 无 | ❌ 无 | ❌ 无 |
| 包体积 | 🟢 小 | 🔴 大 | 🟢 小 | 🟢 小 | 🟢 极小 |
| 类型安全 | ⚠️ 中 | ✅ 强 | ✅ 强 | ⚠️ 弱 | ⚠️ 弱 |
| SSR | ✅ 简单 | 🔴 复杂 | ✅ 简单 | ✅ 简单 | ✅ 简单 |
五、选型决策树
是否需要严格的组件样式隔离?
├── 是 → 是否需要强类型和动态样式?
│ ├── 是 → vanilla-extract / Stitches
│ └── 否 → CSS Modules
└── 否
├── 是否重视设计系统一致性 + 极致性能?
│ ├── 是 → Tailwind 4
│ └── 否 → UnoCSS
各方案适用场景
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 大型企业后台 | Tailwind / UnoCSS | 复用度高、设计系统化 |
| 设计稿强自由的 C 端 | styled-components | 动态样式灵活 |
| Vue 设计感组件库 | CSS Modules | 与 SFC 配合好 |
| 跨框架 Monorepo | vanilla-extract | 类型友好、零运行时 |
| 极致性能要求 | UnoCSS / Tailwind | 零运行时,CSS 最小 |
六、性能深度剖析
6.1 首屏渲染
- CSS Modules / Tailwind:CSS 静态加载,HTML 渲染后立即应用样式
- CSS-in-JS 运行时:JS 需先下载执行才能生成 CSS,存在 FOUC 风险
6.2 重新渲染性能
在 1k 组件重渲染场景下:
- styled-components:~120ms(每次 props 变化都重新计算样式字符串)
- Tailwind:~5ms(className 字符串拼接)
6.3 CSS 体积
- Tailwind/UnoCSS:通常 10-30KB(gzip)
- CSS Modules:按页面加载,单页面可能更小
- styled-components:CSS + 运行时 JS,总体积大
七、迁移与共存
// before: styled-components
const Btn = styled.button<{ primary?: boolean }>`
background: ${p => p.primary ? '#3b82f6' : '#6b7280'};
color: white;
padding: 8px 16px;
border-radius: 4px;
`
// after: Tailwind
function Btn({ primary, children }) {
return (
<button className={cn(
'px-4 py-2 rounded text-white hover:opacity-90',
primary ? 'bg-blue-500' : 'bg-gray-500',
)}>
{children}
</button>
)
}
八、未来趋势
CSS Houdini + CSS Properties & Values API
@property --color-primary {
syntax: '<color>';
initial-value: #3b82f6;
inherits: true;
}
/* JS 中只需更新变量 */
document.documentElement.style.setProperty('--color-primary', '#ef4444')
原生 CSS Nesting
.button {
background: blue;
&:hover { background: darkblue; }
&.primary { background: red; }
}
九、总结
三大方案各有定位:
- CSS Modules —— 经典、稳定、低开销
- CSS-in-JS —— 动态灵活,建议优先选零运行时方案
- 原子化 CSS —— 性能极致、设计系统化
没有最好的方案,只有最合适的方案。选型时关注:团队规模、项目生命周期、性能要求、设计系统自由度。
Comments | 0条评论