跳到主要内容

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

特性RuntimeZero-Runtime
代表styled-components、Emotionvanilla-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 CSSUnoCSS
类名规则固定可自定义
扫描方式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 ModulesCSS-in-JS
文件独立 .module.css写在 JS/TS 中
运行时有(Runtime 方案)
动态样式通过 className 切换通过 props 动态生成
学习成本低(就是 CSS)中(新的 API)
适用框架通用主要 React

CSS Modules 更轻量、无运行时,适合不需要高度动态样式的项目。

Q4: 什么是原子化 CSS?为什么流行?

答案

原子化 CSS 每个类只做一件事(p-4 = padding: 1rem)。流行原因:

  1. CSS 体积不增长:项目再大,只要用的工具类固定,CSS 体积几乎不变
  2. 无命名烦恼:不用思考 .card-wrapper-title-text 这种类名
  3. 设计一致性:受限的值域(spacing、color scale)保证一致
  4. 快速开发:不用切换 HTML/CSS 文件

缺点是需要学习工具类名称,HTML 类名较长。

Q5: 为什么 React 推荐不在 Server Components 中使用 Runtime CSS-in-JS?

答案

Runtime CSS-in-JS(如 styled-components)需要:

  1. React Context 传递主题(Server Components 不支持 Context)
  2. 运行时注入 <style> 标签(Server Components 不运行在浏览器中)
  3. 依赖 useState/useEffect 等 Hooks(Server Components 不支持)

替代方案:

  • Tailwind CSS:纯 CSS 类名,完美兼容
  • CSS Modules:构建时处理,无运行时
  • vanilla-extract:零运行时,构建时生成 CSS

Q6: 项目中如何选择 CSS 方案?

答案

场景推荐方案
新 React 项目Tailwind CSS 或 CSS Modules
Next.js / RSCTailwind 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' })

相关链接