CSS-in-JS 与原子化 CSS
问题
什么是 CSS-in-JS?什么是原子化 CSS?Tailwind CSS、styled-components、CSS Modules 各有什么优缺点?
答案
CSS 方案全景
CSS-in-JS
使用 JavaScript 编写 CSS,样式与组件绑定。
Runtime CSS-in-JS(运行时)
在运行时生成和注入样式。
styled-components
import styled from 'styled-components';
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
}
const Button = styled.button<ButtonProps>`
padding: ${({ size }) => {
switch (size) {
case 'sm': return '4px 8px';
case 'lg': return '12px 24px';
default: return '8px 16px';
}
}};
background: ${({ variant }) =>
variant === 'primary' ? '#3b82f6' : '#e5e7eb'};
color: ${({ variant }) =>
variant === 'primary' ? 'white' : '#333'};
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
opacity: 0.9;
}
`;
// 使用
function App() {
return <Button variant="primary" size="lg">Click</Button>;
}
Emotion css prop
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
const buttonStyle = css`
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
`;
function App() {
return <button css={buttonStyle}>Click</button>;
}
Zero-Runtime CSS-in-JS(零运行时)
在构建时生成 CSS 文件,无运行时开销。
vanilla-extract(.css.ts)
import { style, createTheme } from '@vanilla-extract/css';
// 类型安全的主题
export const [themeClass, vars] = createTheme({
color: {
primary: '#3b82f6',
text: '#1a1a1a',
},
space: {
sm: '8px',
md: '16px',
},
});
// 样式定义
export const button = style({
padding: vars.space.md,
background: vars.color.primary,
color: 'white',
borderRadius: '4px',
':hover': {
opacity: 0.9,
},
});
Runtime vs Zero-Runtime
| 特性 | Runtime | Zero-Runtime |
|---|---|---|
| 代表 | styled-components、Emotion | vanilla-extract、Panda CSS |
| 样式生成时机 | 运行时 | 构建时 |
| 性能 | 有运行时开销 | 无运行时开销 |
| 动态样式 | 灵活 | 有限制 |
| 类型安全 | 一般 | 强 |
| SSR | 需要额外配置 | 天然支持 |
| 包体积 | 需要运行时库 | 只有 CSS 输出 |
Runtime CSS-in-JS 的性能问题
Runtime CSS-in-JS(styled-components、Emotion)在每次渲染时都要序列化样式、注入 <style> 标签,在高频更新场景下会影响性能。
React 官方推荐在服务端组件中不要使用 Runtime CSS-in-JS。新项目建议使用 Zero-Runtime 方案或原子化 CSS。
原子化 CSS
每个 CSS 类只包含一个样式规则:
/* 原子化 CSS 的核心思想 */
.flex { display: flex; }
.items-center { align-items: center; }
.p-4 { padding: 1rem; }
.bg-blue-500 { background: #3b82f6; }
.text-white { color: white; }
Tailwind CSS
最流行的原子化 CSS 框架:
Tailwind CSS 示例
function Card({ title, description }: { title: string; description: string }) {
return (
<div className="rounded-lg border border-gray-200 p-6 shadow-sm hover:shadow-md transition-shadow">
<h2 className="text-xl font-bold text-gray-900 mb-2">{title}</h2>
<p className="text-gray-600 leading-relaxed">{description}</p>
<button className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
了解更多
</button>
</div>
);
}
响应式 + 暗色模式
<div className="
p-4 md:p-8 lg:p-12
bg-white dark:bg-gray-900
text-gray-900 dark:text-gray-100
grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4
">
{/* 内容 */}
</div>
UnoCSS
比 Tailwind 更灵活的原子化引擎,按需生成:
UnoCSS 示例
<div className="flex items-center gap-4 p-4 rounded-lg bg-blue-50">
<span className="text-2xl font-bold text-blue-600">Title</span>
</div>
{/* UnoCSS 支持属性化模式 */}
<div flex items-center gap-4 p-4 rounded-lg bg="blue-50">
内容
</div>
Tailwind vs UnoCSS
| 特性 | Tailwind CSS | UnoCSS |
|---|---|---|
| 类名规则 | 固定 | 可自定义 |
| 扫描方式 | JIT 编译 | 按需引擎 |
| 性能 | 快 | 更快(5x) |
| 预设 | @tailwindcss/typography 等 | 社区预设丰富 |
| 生态 | 最成熟(Headless UI等) | 快速增长 |
| 框架整合 | 好 | 好 |
CSS Modules
自动生成唯一类名,作用域隔离:
Card.module.css
.card {
padding: 24px;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 8px;
}
/* 组合(composes) */
.primaryCard {
composes: card;
border-color: #3b82f6;
}
Card.tsx
import styles from './Card.module.css';
function Card({ title }: { title: string }) {
return (
<div className={styles.card}>
<h2 className={styles.title}>{title}</h2>
</div>
);
}
方案比较总览
| 方案 | 学习成本 | 性能 | 类型安全 | SSR | 维护性 | 适用场景 |
|---|---|---|---|---|---|---|
| Tailwind CSS | 中 | 🟢 优 | 中 | 🟢 | 好 | 快速开发 |
| CSS Modules | 低 | 🟢 优 | 低 | 🟢 | 好 | 传统项目 |
| vanilla-extract | 中 | 🟢 优 | 🟢 强 | 🟢 | 好 | TS 项目 |
| styled-components | 低 | 🟡 中 | 中 | 🟡 | 中 | React CSR |
| Sass/Less | 低 | 🟢 优 | 低 | 🟢 | 中 | 传统项目 |
| UnoCSS | 中 | 🟢 优 | 中 | 🟢 | 好 | 灵活需求 |
常见面试问题
Q1: CSS-in-JS 有什么优缺点?
答案:
优点:
- 样式与组件共存,无全局命名冲突
- 可使用 JS 的变量、函数、条件逻辑
- 自动添加浏览器前缀
- 删除组件时样式自动清理,无死代码
缺点:
- Runtime 方案有性能开销(序列化、注入)
- 增加包体积(需要运行时库)
- SSR 需要额外配置
- DevTools 中类名不易阅读
- React Server Components 不兼容 Runtime CSS-in-JS
Q2: Tailwind CSS 的优缺点?
答案:
优点:
- 开发效率高,不用起类名
- 产物体积小(只包含使用的类)
- 一致的设计系统(间距、颜色)
- 响应式、暗色模式原生支持
- 无 CSS 文件增长问题
缺点:
- 类名冗长,HTML 可读性差
- 学习成本(需要记忆类名)
- 复杂样式需要
@apply或自定义 - 高度定制的设计较难实现
Q3: CSS Modules 和 CSS-in-JS 的区别?
答案:
| 特性 | CSS Modules | CSS-in-JS |
|---|---|---|
| 文件 | 独立 .module.css | 写在 JS/TS 中 |
| 运行时 | 无 | 有(Runtime 方案) |
| 动态样式 | 通过 className 切换 | 通过 props 动态生成 |
| 学习成本 | 低(就是 CSS) | 中(新的 API) |
| 适用框架 | 通用 | 主要 React |
CSS Modules 更轻量、无运行时,适合不需要高度动态样式的项目。
Q4: 什么是原子化 CSS?为什么流行?
答案:
原子化 CSS 每个类只做一件事(p-4 = padding: 1rem)。流行原因:
- CSS 体积不增长:项目再大,只要用的工具类固定,CSS 体积几乎不变
- 无命名烦恼:不用思考
.card-wrapper-title-text这种类名 - 设计一致性:受限的值域(spacing、color scale)保证一致
- 快速开发:不用切换 HTML/CSS 文件
缺点是需要学习工具类名称,HTML 类名较长。
Q5: 为什么 React 推荐不在 Server Components 中使用 Runtime CSS-in-JS?
答案:
Runtime CSS-in-JS(如 styled-components)需要:
- React Context 传递主题(Server Components 不支持 Context)
- 运行时注入
<style>标签(Server Components 不运行在浏览器中) - 依赖
useState/useEffect等 Hooks(Server Components 不支持)
替代方案:
- Tailwind CSS:纯 CSS 类名,完美兼容
- CSS Modules:构建时处理,无运行时
- vanilla-extract:零运行时,构建时生成 CSS
Q6: 项目中如何选择 CSS 方案?
答案:
| 场景 | 推荐方案 |
|---|---|
| 新 React 项目 | Tailwind CSS 或 CSS Modules |
| Next.js / RSC | Tailwind CSS / CSS Modules / vanilla-extract |
| 组件库 | CSS-in-JS(styled-components)或 CSS 变量 |
| Vue 项目 | <style scoped> + Sass 或 UnoCSS |
| 强类型需求 | vanilla-extract |
| 已有大项目 | 保持现有方案,渐进迁移 |
| 快速原型 | Tailwind CSS |
Q7: Tailwind CSS 中如何复用样式?
答案:
方式1: @apply(不推荐过度使用)
.btn-primary {
@apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}
方式2: 组件抽象(推荐)
function Button({ children, variant = 'primary' }: ButtonProps) {
const baseClasses = 'px-4 py-2 rounded font-medium transition-colors';
const variants = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
};
return (
<button className={`${baseClasses} ${variants[variant]}`}>
{children}
</button>
);
}
方式3: cva(class-variance-authority)
import { cva } from 'class-variance-authority';
const button = cva('px-4 py-2 rounded font-medium', {
variants: {
intent: {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800',
},
size: {
sm: 'text-sm px-2 py-1',
lg: 'text-lg px-6 py-3',
},
},
defaultVariants: {
intent: 'primary',
size: 'sm',
},
});
// button({ intent: 'primary', size: 'lg' })