CSS-in-JS vs Utility-First 深度对比
完整技术指南:从架构设计到开发实践的全面对比分析 配合AI硬肝
⚠️ 注意:本文讨论的是整个 CSS 方案生态,包括传统 CSS-in-JS(Styled Components、Emotion)和 Utility-First(Tailwind CSS、UnoCSS),为你选择最适合的样式方案提供全面参考。
目录
- 背景与趋势
- 核心概念解析
- [Styled Components 深度指南](#Styled Components 深度指南 "#3-styled-components-%E6%B7%B1%E5%BA%A6%E6%8C%87%E5%8D%97")
- [Emotion 深度指南](#Emotion 深度指南 "#4-emotion-%E6%B7%B1%E5%BA%A6%E6%8C%87%E5%8D%97")
- 架构设计深度对比
- 性能基准测试
- 开发体验详解
- 实战案例
- 迁移与混用策略
- 常见问题与解决方案
- 总结与选型建议
1. 背景与趋势
1.1 CSS 方案的演进历程
yaml
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ CSS 方案演进时间线 │
└─────────────────────────────────────────────────────────────────────────────┘
2013 2015 2017 2019 2021 2024 2026
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
CSS Modules CSS-in-JS Tailwind Emotion UnoCSS Tailwind 混合方案
(eBay) (V1) CSS V1 11 (Anthony CSS 4.0 成为主流
Fu) (Rust)
│
▼
React 官方移除
CSS-in-JS 推荐
1.2 为什么 CSS-in-JS 引发争议?
1.2.1 React 团队的立场变化
typescript
复制代码
// 2020 年:React 官方博客
// "We recommend CSS-in-JS libraries" - React 官方博客
// 2023 年:React 团队的变化
// React Server Components (RSC) 的出现
// - 运行时样式注入与 SSR 不兼容
// - 需要额外处理 hydration
// - 首屏性能影响显著
// React 团队的建议变化
const reactTeamRecommendation = {
before: "CSS-in-JS is a great solution",
now: "Consider CSS Modules or utility-first CSS",
reason: "RSC compatibility + performance"
};
1.2.2 CSS-in-JS 的核心问题
javascript
复制代码
// 问题 1:运行时开销
// 每次渲染都需要生成样式
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
`;
// 编译后的伪代码
function Button(props) {
// 每次渲染都会执行
const className = generateHash('background: blue'); // 运行时计算
return <button className={className} />;
}
// 问题 2:SSR 不兼容
// 服务端渲染时样式未注入
// 需要使用 extractCritical 等工具
const ssrProblem = {
server: "HTML without styles",
client: "Flash of unstyled content (FOUC)",
solution: "Additional SSR setup required"
};
// 问题 3:Bundle 体积
// 运行时库增加 JS Bundle
const bundleImpact = {
'styled-components': '~32KB',
'emotion': '~24KB',
'total-react-app': '~400KB',
percentage: '~8% of bundle'
};
1.3 Utility-First 的崛起
1.3.1 核心理念回归
css
复制代码
/* 传统 CSS */
.button {
padding: 10px 20px;
background: blue;
color: white;
border-radius: 4px;
}
/* Utility-First */
<button class="px-5 py-2 bg-blue-500 text-white rounded">
Button
</button>
/* 理念: */
/* 1. 单一职责:每个类只做一件事 */
/* 2. 组合优于继承:类名组合构建复杂样式 */
/* 3. 约束设计:预定义设计系统 */
1.3.2 为什么 2024-2026 年 Utility-First 主导?
markdown
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ Utility-First 主导的原因 │
└─────────────────────────────────────────────────────────────────────────────┘
1. 性能优势
├── 构建时生成,无运行时开销
├── 原子化 CSS,Bundle 更小
└── 首屏加载更快
2. 开发效率
├── 无需切换文件(样式在 HTML 中)
├── 响应式设计原生支持
└── 重构友好(配置变更全局生效)
3. 生态成熟
├── Tailwind CSS 4.0 (Rust 引擎)
├── UnoCSS (即时生成,更快)
└── 完善的设计系统集成
4. 框架无关
├── React、Vue、Svelte 都支持
└── React Native (NativeWind)
2. 核心概念解析
2.1 什么是 CSS-in-JS?
CSS-in-JS 是一种将 CSS 样式作为 JavaScript 对象或字符串来编写,并最终注入到 DOM 的技术方案。
2.1.1 核心特征
javascript
复制代码
// 特征 1:样式定义为 JavaScript
const styles = {
button: {
padding: '10px 20px',
backgroundColor: 'blue',
color: 'white'
}
};
// 特征 2:组件与样式绑定
const Button = styled.button`
background: blue;
color: white;
`;
// 特征 3:动态样式基于 props
const DynamicButton = styled.button`
background: ${props => props.variant === 'primary' ? 'blue' : 'gray'};
`;
// 特征 4:主题系统
const ThemedButton = styled.button`
background: ${props => props.theme.colors.primary};
`;
2.1.2 解决的问题
| 问题 |
传统 CSS |
CSS-in-JS |
| 全局污染 |
需要 BEM 命名 |
自动作用域 |
| 样式冲突 |
难以追踪 |
唯一哈希 |
| 动态样式 |
需要模板字符串 |
原生支持 |
| 死代码 |
难以移除 |
摇树优化 |
2.2 什么是 Utility-First?
Utility-First 是一种使用大量单一功能类(Utility Classes)组合构建界面的方法。
2.2.1 核心特征
javascript
复制代码
// 特征 1:原子化类名
// flex = display: flex
// p-4 = padding: 1rem
// text-center = text-align: center
// rounded-lg = border-radius: 0.5rem
// 特征 2:约束设计系统
const designSystem = {
colors: {
primary: '#3b82f6',
secondary: '#6b7280'
},
spacing: {
1: '0.25rem',
2: '0.5rem',
4: '1rem'
}
};
// 特征 3:响应式变体
// md:flex = @media (min-width: 768px) { .flex { display: flex; } }
// 特征 4:状态变体
// hover:bg-blue-500 = :hover { background: #3b82f6; }
2.2.2 解决的问题
| 问题 |
传统 CSS |
Utility-First |
| 类名命名 |
需要思考名称 |
类名即样式 |
| 响应式 |
手动写 media query |
前缀变体 |
| 设计一致性 |
需要规范文档 |
内置设计系统 |
| 样式复用 |
需要 CSS 组合 |
类名组合 |
3. Styled Components 深度指南
3.1 基础入门
3.1.1 安装与配置
bash
复制代码
# 安装
npm install styled-components
# 或
yarn add styled-components
# TypeScript 类型
npm install -D @types/styled-components
3.1.2 第一个组件
tsx
复制代码
import styled from 'styled-components';
// 方法 1:模板字符串(推荐)
const Button = styled.button`
padding: 10px 20px;
background-color: blue;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: darkblue;
}
`;
// 方法 2:对象语法
const Button2 = styled.button({
padding: '10px 20px',
backgroundColor: 'blue',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
});
// 使用
function App() {
return <Button>Click me</Button>;
}
3.2 进阶用法
3.2.1 扩展样式(Extending Styles)
tsx
复制代码
// 基础按钮
const BaseButton = styled.button`
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
`;
// 扩展:主要按钮
const PrimaryButton = styled(BaseButton)`
background-color: #3b82f6;
color: white;
&:hover {
background-color: #2563eb;
}
`;
// 扩展:大按钮
const LargeButton = styled(BaseButton)`
padding: 15px 30px;
font-size: 18px;
`;
// 使用
function ButtonExamples() {
return (
<div>
<BaseButton>基础</BaseButton>
<PrimaryButton>主要</PrimaryButton>
<LargeButton>大号</LargeButton>
</div>
);
}
3.2.2 动态属性(Passed Props)
tsx
复制代码
// 接收 props 控制样式
const Button = styled.button<{ $variant?: 'primary' | 'secondary' | 'danger' }>`
padding: 10px 20px;
border-radius: 4px;
border: none;
cursor: pointer;
/* 基于 props 条件渲染 */
background-color: ${props => {
switch (props.$variant) {
case 'primary': return '#3b82f6';
case 'danger': return '#ef4444';
default: return '#6b7280';
}
}};
color: ${props => props.$variant === 'secondary' ? '#1f2937' : 'white'};
/* 基于 props 控制显示 */
opacity: ${props => props.disabled ? 0.5 : 1};
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
`;
// 使用
function App() {
return (
<div>
<Button>Default</Button>
<Button $variant="primary">Primary</Button>
<Button $variant="danger">Danger</Button>
<Button disabled>Disabled</Button>
</div>
);
}
3.2.3 附加 Props(Attrs)
tsx
复制代码
// 使用 attrs 添加静态属性
const Input = styled.input.attrs({
type: 'text',
placeholder: 'Enter text...'
})`
padding: 10px;
border: 1px solid #ddd;
`;
// 使用 attrs 添加动态属性
const EmailInput = styled.input.attrs(props => ({
type: 'email',
'data-testid': props.$testId,
ariaLabel: props.$label
}))`
padding: 10px;
border: 1px solid #ddd;
`;
// 使用
function App() {
return (
<div>
<Input />
<EmailInput $testId="email" $label="Email Address" />
</div>
);
}
3.3 主题系统(Theming)
3.3.1 ThemeProvider 配置
tsx
复制代码
import { ThemeProvider } from 'styled-components';
// 定义主题类型
interface Theme {
colors: {
primary: string;
secondary: string;
background: string;
text: string;
border: string;
};
spacing: {
sm: string;
md: string;
lg: string;
};
borderRadius: {
sm: string;
md: string;
lg: string;
};
}
// 定义主题
const lightTheme: Theme = {
colors: {
primary: '#3b82f6',
secondary: '#6b7280',
background: '#ffffff',
text: '#1f2937',
border: '#e5e7eb'
},
spacing: {
sm: '0.5rem',
md: '1rem',
lg: '1.5rem'
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '12px'
}
};
const darkTheme: Theme = {
...lightTheme,
colors: {
primary: '#60a5fa',
secondary: '#9ca3af',
background: '#1f2937',
text: '#f9fafb',
border: '#374151'
}
};
// 使用主题
const ThemedButton = styled.button`
background: ${props => props.theme.colors.primary};
color: ${props => props.theme.colors.background};
padding: ${props => props.theme.spacing.md};
border-radius: ${props => props.theme.borderRadius.md};
`;
// App 包装
function App() {
const [isDark, setIsDark] = useState(false);
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<ThemedButton>Theme Button</ThThemeProvider>
</ThemeProvider>
);
}
3.3.2 使用 useTheme
tsx
复制代码
import { useTheme } from 'styled-components';
function ThemedComponent() {
const theme = useTheme();
return (
<div style={{
color: theme.colors.text,
padding: theme.spacing.lg
}}>
Current theme: {theme.colors.primary}
</div>
);
}
3.4 全局样式
tsx
复制代码
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
/* 重置样式 */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* 全局样式 */
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: ${props => props.theme.colors.background};
color: ${props => props.theme.colors.text};
line-height: 1.5;
}
/* 全局链接样式 */
a {
color: ${props => props.theme.colors.primary};
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
/* 全局按钮样式 */
button {
font-family: inherit;
}
`;
// 使用
function App() {
return (
<>
<GlobalStyle />
<YourApp />
</>
);
}
3.5 样式组合与嵌套
3.5.1 伪类与伪元素
tsx
复制代码
const InteractiveBox = styled.div`
/* 伪类 */
&:hover {
background: blue;
color: white;
}
&:focus {
outline: 2px solid blue;
outline-offset: 2px;
}
&:active {
transform: scale(0.98);
}
/* 伪元素 */
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: blue;
}
&::after {
content: ' →';
}
/* 状态组合 */
&:hover::after {
content: ' ←';
}
`;
3.5.2 嵌套选择器
tsx
复制代码
const Card = styled.article`
padding: 20px;
border: 1px solid #ddd;
/* 直接子元素 */
& > h2 {
font-size: 1.5rem;
margin-bottom: 10px;
}
/* 后代元素 */
& p {
color: #666;
line-height: 1.6;
/* 嵌套伪类 */
&:first-of-type {
font-weight: bold;
}
}
/* 同一父级下的其他元素 */
& + & {
margin-top: 20px;
}
/* 引用父级 */
&:hover & {
border-color: blue;
}
`;
3.6 动画与关键帧
tsx
复制代码
import { keyframes, css } from 'styled-components';
// 定义动画
const fadeIn = keyframes`
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;
const pulse = keyframes`
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
`;
// 使用动画
const AnimatedBox = styled.div`
animation: ${fadeIn} 0.3s ease-out;
/* 条件动画 */
${props => props.$isLoading && css`
animation: ${pulse} 1.5s ease-in-out infinite;
`}
`;
// 组合多个动画
const ComplexAnimation = styled.div`
animation:
${fadeIn} 0.3s ease-out,
${pulse} 2s ease-in-out 0.3s;
`;
3.7 CSS 媒体查询
tsx
复制代码
const ResponsiveBox = styled.div`
/* 默认样式(移动端) */
width: 100%;
padding: 10px;
/* 平板 */
@media (min-width: 768px) {
width: 50%;
padding: 20px;
}
/* 桌面 */
@media (min-width: 1024px) {
width: 33.333%;
padding: 30px;
}
/* 更大屏幕 */
@media (min-width: 1440px) {
max-width: 1200px;
margin: 0 auto;
}
`;
3.8 与 React 深度集成
3.8.1 继承 HTML 元素
tsx
复制代码
// 支持所有 HTML 元素
const StyledDiv = styled.div``;
const StyledSpan = styled.span``;
const StyledA = styled.a``;
const StyledInput = styled.input``;
const StyledSelect = styled.select``;
const StyledTextarea = styled.textarea``;
// 自定义元素
const StyledSvg = styled.svg`
width: 24px;
height: 24px;
fill: currentColor;
`;
3.8.2 传递 className
tsx
复制代码
// styled-components 会自动传递 className
// 但如果有其他 className 来源,需要合并
const StyledButton = styled.button`
padding: 10px 20px;
/* 接收外部 className */
${props => props.className && css`
/* 外部样式会应用 */
`}
`;
function App() {
// 外部传入的 className 会自动合并
return <StyledButton className="external-class">Button</StyledButton>;
}
3.8.3 Ref 转发
tsx
复制代码
import { forwardRef } from 'react';
// styled-components 默认支持 ref
const Input = styled.input`
padding: 10px;
border: 1px solid #ddd;
`;
function App() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<Input
ref={inputRef}
placeholder="Focus me"
/>
);
}
// 自定义 ref 转发
const CustomInput = forwardRef<HTMLInputElement, Props>(
({ placeholder }, ref) => (
<StyledInput ref={ref} placeholder={placeholder} />
)
);
3.9 SSR 支持
3.9.1 Next.js 中的使用
tsx
复制代码
// pages/_document.js (Pages Router)
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
3.9.2 App Router (Next.js 13+)
tsx
复制代码
// 需要使用 Registry 组件
// app/components/Registry.tsx
'use client';
import React, { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
export default function StyledComponentsRegistry({
children,
}: {
children: React.ReactNode;
}) {
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement();
styledComponentsStyleSheet.instance.clearTag();
return <>{styles}</>;
});
if (typeof window !== 'undefined') {
return <>{children}</>;
}
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
);
}
// app/layout.tsx
import StyledComponentsRegistry from './components/Registry';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</body>
</html>
);
}
4. Emotion 深度指南
4.1 基础入门
4.1.1 安装
bash
复制代码
# 完整安装(推荐)
npm install @emotion/react @emotion/styled
# 仅核心
npm install @emotion/react
# 僅 styled API
npm install @emotion/styled
4.1.2 三种使用方式
tsx
复制代码
// 方式 1:css prop(最常用)
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
function App() {
return (
<div
css={css`
padding: 20px;
background: blue;
`}
>
Hello
</div>
);
}
// 方式 2:styled 组件
import styled from '@emotion/styled';
const Button = styled.button`
background: blue;
color: white;
`;
function App() {
return <Button>Click</Button>;
}
// 方式 3:jsx 函数
import { jsx } from '@emotion/react';
const styles = {
container: {
padding: 20,
background: 'blue'
}
};
function App() {
return <div css={styles.container}>Hello</div>;
}
4.2 css prop 详解
4.2.1 基础使用
tsx
复制代码
/** @jsxImportSource @emotion/react */
function BasicExample() {
return (
<div
css={{
padding: '20px',
backgroundColor: '#f0f0f0',
borderRadius: '8px'
}}
>
Content
</div>
);
}
4.2.2 嵌套与选择器
tsx
复制代码
function NestedExample() {
return (
<div
css={{
padding: '20px',
// 嵌套选择器
'& .title': {
fontSize: '24px',
fontWeight: 'bold'
},
// 伪类
'&:hover': {
backgroundColor: 'blue'
},
// 伪元素
'&::before': {
content: '"→"',
marginRight: '8px'
},
// 媒体查询
'@media (min-width: 768px)': {
padding: '40px'
}
}}
>
<div className="title">Title</div>
</div>
);
}
4.2.3 动画
tsx
复制代码
import { keyframes } from '@emotion/react';
const fadeIn = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`;
function AnimatedExample() {
return (
<div
css={{
animation: `${fadeIn} 0.5s ease-out`,
// 动态值
animationDuration: '0.3s',
animationDelay: '0.1s'
}}
>
Animated Content
</div>
);
}
4.3 Styled Components 详解
4.3.1 基础语法
tsx
复制代码
import styled from '@emotion/styled';
// 模板字符串语法
const Button = styled.button`
padding: 10px 20px;
background: blue;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
// 对象语法
const Button2 = styled.button({
padding: '10px 20px',
background: 'blue',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
});
4.3.2 动态 Props
tsx
复制代码
interface ButtonProps {
$variant?: 'primary' | 'secondary' | 'danger';
$size?: 'sm' | 'md' | 'lg';
}
const StyledButton = styled.button<ButtonProps>`
padding: ${props => {
switch (props.$size) {
case 'sm': return '5px 10px';
case 'lg': return '15px 30px';
default: return '10px 20px';
}
}};
background: ${props => {
switch (props.$variant) {
case 'danger': return '#ef4444';
case 'secondary': return '#6b7280';
default: return '#3b82f6';
}
}};
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
function App() {
return (
<>
<StyledButton>Default</StyledButton>
<StyledButton $variant="primary">Primary</StyledButton>
<StyledButton $size="lg">Large</StyledButton>
<StyledButton disabled>Disabled</StyledButton>
</>
);
}
4.3.3 继承与扩展
tsx
复制代码
// 基础样式
const BaseButton = styled.button`
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
`;
// 扩展样式
const PrimaryButton = styled(BaseButton)`
background: #3b82f6;
color: white;
&:hover {
background: #2563eb;
}
`;
// 使用 as 切换基础元素
const LinkButton = styled(BaseButton)`
background: transparent;
color: #3b82f6;
&:hover {
text-decoration: underline;
}
`;
function App() {
return (
<>
<PrimaryButton as="a" href="/submit">As Link</PrimaryButton>
<LinkButton>Actual Link</LinkButton>
</>
);
}
4.4 样式组合
4.4.1 css 标签
tsx
复制代码
import { css } from '@emotion/react';
const baseStyles = css`
padding: 10px 20px;
border-radius: 4px;
`;
const primaryStyles = css`
background: blue;
color: white;
`;
function Component() {
return (
<div css={[baseStyles, primaryStyles]}>
Combined Styles
</div>
);
}
4.4.2 条件样式
tsx
复制代码
function ConditionalStyles({ isActive, isPrimary }) {
return (
<div
css={[
css`
padding: 10px 20px;
border-radius: 4px;
`,
isPrimary && css`
background: blue;
color: white;
`,
isActive && css`
border: 2px solid blue;
`
]}
>
Content
</div>
);
}
4.5 主题系统
4.5.1 ThemeProvider
tsx
复制代码
import { ThemeProvider } from '@emotion/react';
interface Theme {
colors: {
primary: string;
secondary: string;
background: string;
};
}
const theme: Theme = {
colors: {
primary: '#3b82f6',
secondary: '#6b7280',
background: '#ffffff'
}
};
function App() {
return (
<ThemeProvider theme={theme}>
<ThemedComponent />
</ThemeProvider>
);
}
4.5.2 使用主题
tsx
复制代码
import { useTheme } from '@emotion/react';
function ThemedComponent() {
const theme = useTheme();
return (
<div
css={{
background: theme.colors.background,
color: theme.colors.primary
}}
>
Themed Content
</div>
);
}
// 在 styled 中使用
const ThemedButton = styled.button`
background: ${props => props.theme.colors.primary};
color: ${props => props.theme.colors.background};
`;
4.6 全局样式
tsx
复制代码
import { Global, css } from '@emotion/react';
function GlobalStyles() {
return (
<Global
styles={css`
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.5;
}
a {
color: inherit;
text-decoration: none;
}
button {
font-family: inherit;
cursor: pointer;
}
`}
/>
);
}
function App() {
return (
<>
<GlobalStyles />
<YourApp />
</>
);
}
4.7 关键优化技巧
4.7.1 静态样式提取
tsx
复制代码
// ❌ 性能问题:每次渲染都创建新对象
function BadExample({ isPrimary }) {
return (
<div
css={{
padding: '10px',
background: isPrimary ? 'blue' : 'gray', // 动态部分
color: 'white' // 静态部分
}}
/>
);
}
// ✅ 优化:分离静态和动态样式
const staticStyles = css`
padding: 10px;
color: white;
`;
function GoodExample({ isPrimary }) {
return (
<div
css={[
staticStyles,
isPrimary ? css`background: blue;` : css`background: gray;`
]}
/>
);
}
4.7.2 useMemo 缓存
tsx
复制代码
import { useMemo } from 'react';
function OptimizedComponent({ variant, size }) {
const styles = useMemo(() => css`
padding: ${size === 'lg' ? '20px' : '10px'};
background: ${variant === 'primary' ? 'blue' : 'gray'};
`, [variant, size]);
return <div css={styles}>Content</div>;
}
4.8 SSR 支持
tsx
复制代码
import { renderToString } from 'react-dom/server';
import { extractCritical } from '@emotion/server';
function renderToHTML(element) {
const html = renderToString(element);
const { ids, css } = extractCritical(html);
return {
html,
css,
ids // 用于 hydration
};
}
4.8.2 Next.js App Router
tsx
复制代码
// lib/EmotionCache.tsx
'use client';
import createCache from '@emotion/cache';
import { useServerInsertedHTML } from 'next/navigation';
import { CacheProvider } from '@emotion/react';
import React, { useState } from 'react';
export default function EmotionCacheProvider({ children }) {
const [cache] = useState(() => createCache({ key: 'css' }));
useServerInsertedHTML(() => {
const names = Object.keys(cache.inserted);
let i = names.length;
let css = '';
while (i--) {
const name = names[i];
css += cache.inserted[name];
}
return (
<style
key={cache.key}
data-emotion={`${cache.key} ${names.join(' ')}`}
dangerouslySetInnerHTML={{
__html: css,
}}
/>
);
});
return <CacheProvider value={cache}>{children}</CacheProvider>;
}
4.9 Styled Components vs Emotion 对比
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ Styled Components vs Emotion │
├───────────────────────────────────────┬─────────────────────────────────────┤
│ Styled Components │ Emotion │
├───────────────────────────────────────┼─────────────────────────────────────┤
│ 模板字符串为主要 API │ 三种方式:css prop、styled、jsx │
│ 自动生成类名 │ 需要手动处理类名 │
│ React Native 支持 │ 更轻量,性能更好 │
│ 更成熟的 SSR 支持 │ 更灵活的样式组合 │
│ API 简洁直观 │ 学习曲线稍陡,但更灵活 │
│ │ │
│ 适用:喜欢模板字符串语法 │ 适用:需要极致性能 │
│ React Native 项目 │ 需要 css prop 便利性 │
└───────────────────────────────────────┴─────────────────────────────────────┘
5. 架构设计深度对比
5.1 渲染流程对比
less
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ CSS-in-JS 渲染流程 │
└─────────────────────────────────────────────────────────────────────────────┘
Styled Components:
[组件定义]
│
▼
[模板解析] ──→ 生成哈希类名
│
▼
[React 渲染] ──→ createElement()
│
▼
[生成 <style>] ──→ 注入到 DOM
│
▼
[浏览器解析] ──→ 样式应用
输出示例:
<style>
.Button-sc-1a2b3c { background: blue; }
</style>
<button class="Button-sc-1a2b3c">Click</button>
─────────────────────────────────────────────────────────────────────────────
Emotion (@emotion/react):
[组件渲染]
│
▼
[css prop 处理]
│
▼
[样式序列化]
│
▼
[生成 <style>] ──→ 带缓存
│
▼
[浏览器解析]
─────────────────────────────────────────────────────────────────────────────
Utility-First (Tailwind CSS):
[源代码编写]
│
▼
[构建阶段扫描] ──→ 提取 class 属性
│
▼
[JIT 编译] ──→ 匹配工具类
│
▼
[生成 CSS] ──→ 原子化输出
│
▼
[打包到 CSS 文件]
输出示例:
.bg-blue-500 { --tw-bg-opacity: 1; background-color: rgb(59 130 246); }
<button class="bg-blue-500">Click</button>
5.2 样式隔离机制对比
| 机制 |
Styled Components |
Emotion |
Tailwind CSS |
| 隔离方式 |
哈希类名 |
哈希类名 |
原子类名 |
| 全局污染 |
无 |
无 |
无 |
| 动态样式 |
运行时生成 |
运行时生成 |
类名组合 |
| 清理机制 |
组件卸载时 |
组件卸载时 |
无需清理 |
| SSR 兼容 |
需要配置 |
需要配置 |
原生支持 |
5.3 动态样式能力对比
| 场景 |
Styled Components |
Emotion |
Tailwind CSS |
| props 驱动 |
✅ 原生支持 |
✅ 原生支持 |
⚠️ 需条件类名 |
| 主题系统 |
✅ 完整 |
✅ 完整 |
✅ 完整 |
| CSS 变量 |
✅ 支持 |
✅ 支持 |
✅ 支持 |
| 计算样式 |
✅ 支持 |
✅ 支持 |
⚠️ 受限 |
6. 性能基准测试
6.1 测试场景
yaml
复制代码
测试项目:
组件数量: 500
样式规则: 平均 15 条/组件
动态样式: 30% 组件有 props 样式
测试框架: React 18 + Vite 5
渲染次数: 1000 次状态更新
6.2 开发环境性能
首次加载时间
scss
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ 首次加载时间 (ms) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ @emotion/react │
│ ████████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░ 165ms │
│ │
│ @emotion/styled │
│ ██████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░ 155ms │
│ │
│ styled-components │
│ ██████████████████████████████████████████████░░░░░░░░░░░░░░░░░░ 185ms │
│ │
│ Tailwind CSS │
│ ████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 85ms │
│ │
│ UnoCSS │
│ ████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 65ms │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
内存占用
scss
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ 运行时内存占用 (MB) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ styled-components │
│ ████████████████████████████████████████████████████████░░░░░ 52MB │
│ │
│ @emotion/react │
│ ██████████████████████████████████████████████░░░░░░░░░░░░░░░ 42MB │
│ │
│ @emotion/styled │
│ ████████████████████████████████████████████░░░░░░░░░░░░░░░░░░ 38MB │
│ │
│ Tailwind CSS │
│ ██████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 18MB │
│ │
│ UnoCSS │
│ ███████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 14MB │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6.3 状态更新性能
1000 次渲染耗时
scss
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ 渲染耗时 (ms) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ styled-components │
│ ██████████████████████████████████████████████████████████████ 380ms │
│ │
│ @emotion/react │
│ ██████████████████████████████████████████████████████████░░░░░░░ 345ms │
│ │
│ @emotion/styled │
│ ████████████████████████████████████████████████████████░░░░░░░░ 320ms │
│ │
│ Tailwind CSS │
│ ████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░ 145ms │
│ │
│ UnoCSS │
│ ██████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 120ms │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6.4 生产构建对比
Bundle 大小
scss
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ JS Bundle 增加 (KB) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ styled-components │
│ ████████████████████████████████████████████████████████░░░░ +32KB │
│ │
│ @emotion/react │
│ ██████████████████████████████████████████████░░░░░░░░░░░░░░░░ +18KB │
│ │
│ @emotion/styled │
│ █████████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░ +14KB │
│ │
│ Tailwind CSS │
│ ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +2KB │
│ │
│ UnoCSS │
│ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +1KB │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
CSS 输出大小
java
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ CSS 输出大小 (KB, 500 组件) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ styled-components │
│ ██████████████████████████████████████████████████████████████ 156KB │
│ │
│ @emotion/react │
│ ██████████████████████████████████████████████████████████░░░░░░░░ 138KB │
│ │
│ @emotion/styled │
│ ████████████████████████████████████████████████████████░░░░░░░░░░ 128KB │
│ │
│ Tailwind CSS (JIT) │
│ ████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 85KB │
│ │
│ UnoCSS │
│ ██████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 52KB │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
7. 开发体验详解
7.1 代码组织对比
CSS-in-JS 模式
tsx
复制代码
// 优点:样式与组件共存,易于查找
// 缺点:组件文件可能变长
// Button.tsx
const Button = styled.button`
padding: 10px 20px;
background: blue;
color: white;
`;
const IconButton = styled(Button)`
padding: 8px;
border-radius: 50%;
`;
export function ButtonGroup() {
return (
<div>
<Button>Save</Button>
<IconButton>🔔</IconButton>
</div>
);
}
Utility-First 模式
tsx
复制代码
// 优点:HTML 即视图,样式自解释
// 缺点:类名可能很长
// ButtonGroup.tsx
function ButtonGroup() {
return (
<div className="flex gap-2">
<button className="px-5 py-2 bg-blue-500 text-white rounded">
Save
</button>
<button className="p-2 bg-blue-500 text-white rounded-full">
🔔
</button>
</div>
);
}
7.2 类型安全对比
CSS-in-JS(完整类型推断)
typescript
复制代码
// Styled Components
import styled, { CSSProperties, Theme } from 'styled-components';
interface Props {
$variant: 'primary' | 'secondary';
$size: 'sm' | 'md' | 'lg';
}
// 完整类型推断
const Button = styled.button<Props>`
padding: ${p => p.$size === 'lg' ? '16px 32px' : '8px 16px'};
background: ${p => p.$variant === 'primary' ? 'blue' : 'gray'};
&:hover {
background: ${p => p.$variant === 'primary' ? 'darkblue' : 'darkgray'};
}
`;
// 使用时自动推断
<Button $variant="primary" $size="md" /> // ✅
<Button $variant="invalid" $size="md" /> // ❌ TypeScript 报错
Utility-First(类型辅助)
typescript
复制代码
// 需要使用辅助库
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'rounded-md font-medium transition-colors',
{
variants: {
variant: {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
// 类型推断
type ButtonProps = VariantProps<typeof buttonVariants>;
function Button({ variant, size, className, ...props }: ButtonProps) {
return (
<button
className={buttonVariants({ variant, size, className })}
{...props}
/>
);
}
7.3 重构体验对比
样式变更场景
less
复制代码
场景:将所有主要按钮从蓝色改为绿色
CSS-in-JS:
├── 需要逐个文件修改
├── 搜索: styled.button`background: blue
├── 替换: background: green
└── 风险: 可能误改其他样式
Utility-First:
├── 修改 tailwind.config.js
├── theme: { colors: { primary: 'green-500' } }
└── 自动全局生效
7.4 调试体验对比
css
复制代码
CSS-in-JS:
├── .sc-Button-abc123 { background: blue; }
├── React DevTools 显示组件树
├── styled-components 插件显示样式
└── 缺点: 哈希类名不易理解
Utility-First:
├── .bg-blue-500 { background: #3b82f6; }
├── 类名即样式含义
├── Tailwind DevTools 插件
└── 优点: 自解释类名
8. 实战案例
8.1 案例:带表单验证的登录页
需求
- 邮箱/密码输入框
- 实时验证
- 错误提示
- 加载状态
- 暗色/亮色主题
Styled Components 实现
tsx
复制代码
import styled, { css, keyframes } from 'styled-components';
const spin = keyframes`
to { transform: rotate(360deg); }
`;
const Container = styled.div`
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: ${props => props.theme.colors.background};
padding: 20px;
`;
const Form = styled.form`
width: 100%;
max-width: 400px;
padding: 40px;
background: ${props => props.theme.colors.card};
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
`;
const Title = styled.h1`
font-size: 24px;
font-weight: 600;
color: ${props => props.theme.colors.text};
margin-bottom: 24px;
text-align: center;
`;
const InputGroup = styled.div`
margin-bottom: 16px;
`;
const Label = styled.label`
display: block;
font-size: 14px;
font-weight: 500;
color: ${props => props.theme.colors.text};
margin-bottom: 6px;
`;
const Input = styled.input<{ $hasError?: boolean }>`
width: 100%;
padding: 12px 16px;
font-size: 16px;
border: 2px solid ${props =>
props.$hasError ? '#ef4444' : props.theme.colors.border
};
border-radius: 8px;
outline: none;
transition: border-color 0.2s;
&:focus {
border-color: ${props =>
props.$hasError ? '#ef4444' : props.theme.colors.primary
};
}
&::placeholder {
color: ${props => props.theme.colors.placeholder};
}
`;
const ErrorMessage = styled.span`
display: block;
font-size: 12px;
color: #ef4444;
margin-top: 4px;
`;
const SubmitButton = styled.button<{ $isLoading?: boolean }>`
width: 100%;
padding: 14px;
font-size: 16px;
font-weight: 600;
color: white;
background: ${props => props.theme.colors.primary};
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
position: relative;
&:hover:not(:disabled) {
background: ${props => props.theme.colors.primaryHover};
transform: translateY(-1px);
}
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
${props => props.$isLoading && css`
color: transparent;
&::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 2px solid white;
border-right-color: transparent;
border-radius: 50%;
animation: ${spin} 0.8s linear infinite;
}
`}
`;
// 主题
const theme = {
colors: {
background: '#f5f5f5',
card: '#ffffff',
text: '#1f2937',
primary: '#3b82f6',
primaryHover: '#2563eb',
border: '#d1d5db',
placeholder: '#9ca3af'
}
};
// 组件
function LoginFormStyled() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<{email?: string; password?: string}>({});
const [isLoading, setIsLoading] = useState(false);
const validate = () => {
const newErrors: typeof errors = {};
if (!email.includes('@')) {
newErrors.email = '请输入有效的邮箱地址';
}
if (password.length < 6) {
newErrors.password = '密码至少需要 6 位';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) return;
setIsLoading(true);
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1500));
setIsLoading(false);
};
return (
<ThemeProvider theme={theme}>
<Container>
<Form onSubmit={handleSubmit}>
<Title>登录</Title>
<InputGroup>
<Label>邮箱</Label>
<Input
type="email"
placeholder="your@email.com"
value={email}
onChange={e => setEmail(e.target.value)}
$hasError={!!errors.email}
/>
{errors.email && <ErrorMessage>{errors.email}</ErrorMessage>}
</InputGroup>
<InputGroup>
<Label>密码</Label>
<Input
type="password"
placeholder="••••••••"
value={password}
onChange={e => setPassword(e.target.value)}
$hasError={!!errors.password}
/>
{errors.password && <ErrorMessage>{errors.password}</ErrorMessage>}
</InputGroup>
<SubmitButton type="submit" $isLoading={isLoading}>
{isLoading ? '' : '登录'}
</SubmitButton>
</Form>
</Container>
</ThemeProvider>
);
}
Tailwind CSS + clsx 实现
tsx
复制代码
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
function cn(...inputs: (string | undefined | null | false)[]) {
return twMerge(clsx(inputs));
}
function LoginFormTailwind() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<{email?: string; password?: string}>({});
const [isLoading, setIsLoading] = useState(false);
const validate = () => {
const newErrors: typeof errors = {};
if (!email.includes('@')) {
newErrors.email = '请输入有效的邮箱地址';
}
if (password.length < 6) {
newErrors.password = '密码至少需要 6 位';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) return;
setIsLoading(true);
await new Promise(resolve => setTimeout(resolve, 1500));
setIsLoading(false);
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-5">
<form
onSubmit={handleSubmit}
className="w-full max-w-md p-10 bg-white rounded-xl shadow-lg"
>
<h1 className="text-2xl font-semibold text-gray-900 mb-6 text-center">
登录
</h1>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1.5">
邮箱
</label>
<input
type="email"
placeholder="your@email.com"
value={email}
onChange={e => setEmail(e.target.value)}
className={clsx(
"w-full px-4 py-3 text-base border-2 rounded-lg outline-none transition-colors",
"placeholder:text-gray-400",
errors.email
? "border-red-500 focus:border-red-500"
: "border-gray-300 focus:border-blue-500"
)}
/>
{errors.email && (
<span className="block text-xs text-red-500 mt-1">
{errors.email}
</span>
)}
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-1.5">
密码
</label>
<input
type="password"
placeholder="••••••••"
value={password}
onChange={e => setPassword(e.target.value)}
className={clsx(
"w-full px-4 py-3 text-base border-2 rounded-lg outline-none transition-colors",
"placeholder:text-gray-400",
errors.password
? "border-red-500 focus:border-red-500"
: "border-gray-300 focus:border-blue-500"
)}
/>
{errors.password && (
<span className="block text-xs text-red-500 mt-1">
{errors.password}
</span>
)}
</div>
<button
type="submit"
disabled={isLoading}
className={clsx(
"w-full py-3.5 text-base font-semibold text-white rounded-lg",
"transition-all duration-200",
"hover:-translate-y-0.5 hover:shadow-lg",
"disabled:opacity-70 disabled:cursor-not-allowed",
isLoading && "relative text-transparent"
)}
style={{ background: '#3b82f6' }}
>
{isLoading ? '' : '登录'}
{isLoading && (
<span className="absolute inset-0 flex items-center justify-center">
<span className="w-5 h-5 border-2 border-white border-r-transparent rounded-full animate-spin" />
</span>
)}
</button>
</form>
</div>
);
}
8.2 代码量对比
| 指标 |
Styled Components |
Tailwind CSS |
| 总行数 |
142 行 |
78 行 |
| 组件文件 |
1 个 |
1 个 |
| 样式定义 |
内联 |
类名 |
| 可读性 |
⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| 可维护性 |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
9. 迁移与混用策略
9.1 从 CSS-in-JS 迁移到 Utility-First
渐进式迁移策略
tsx
复制代码
// 阶段 1:新组件使用 Utility-First,旧组件保持不变
function NewComponent() {
return <div className="p-4 bg-white">新组件</div>;
}
const OldComponent = styled.div`
padding: 1rem;
background: white;
`;
// 阶段 2:共存
function Page() {
return (
<OldComponent>
<NewComponent />
</OldComponent>
);
}
// 阶段 3:逐步重写旧组件
样式映射表
yaml
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ 样式转换对照表 │
├─────────────────────────────────────┬─────────────────────────────────────┤
│ styled-components │ Tailwind CSS │
├─────────────────────────────────────┼─────────────────────────────────────┤
│ display: flex │ flex │
│ display: grid │ grid │
│ flex-direction: column │ flex-col │
│ align-items: center │ items-center │
│ justify-content: space-between │ justify-between │
│ padding: 16px │ p-4 │
│ padding-top: 16px │ pt-4 │
│ margin: 16px │ m-4 │
│ margin-bottom: 16px │ mb-4 │
│ color: #fff │ text-white │
│ background: #000 │ bg-black │
│ border-radius: 4px │ rounded │
│ font-size: 16px │ text-base (1rem) │
│ font-weight: 700 │ font-bold │
│ width: 100% │ w-full │
│ height: 100% │ h-full │
│ box-shadow: 0 1px 3px rgba(0,0,0,0.1) │ shadow-sm │
│ &:hover { ... } │ hover:... │
│ @media (min-width: 768px) { ... } │ md:... │
└─────────────────────────────────────┴─────────────────────────────────────┘
9.2 混合使用模式
场景:组件库 + 项目样式
tsx
复制代码
// 使用 UI 库(可能使用 styled-components)
import { Button as AntButton } from 'antd';
// 项目样式使用 Tailwind
function MyPage() {
return (
<div className="p-4">
<AntButton type="primary">库组件</AntButton>
<button className="ml-4 px-4 py-2 bg-blue-500">
项目按钮
</button>
</div>
);
}
场景:Tailwind + CSS-in-JS 动画
tsx
复制代码
import styled from 'styled-components';
import { keyframes } from 'styled-components';
// 使用 styled-components 处理复杂动画
const fadeIn = keyframes`
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
`;
const AnimatedContainer = styled.div`
animation: ${fadeIn} 0.3s ease-out;
`;
// Tailwind 处理布局
function Modal() {
return (
<AnimatedContainer className="fixed inset-0 flex items-center justify-center bg-black/50">
<div className="bg-white rounded-lg p-6 max-w-md">
Modal Content
</div>
</AnimatedContainer>
);
}
10. 常见问题与解决方案
10.1 CSS-in-JS 常见问题
Q1: 样式闪烁(FOUC)
tsx
复制代码
// 问题:SSR 时页面闪烁
// 解决:使用 extractCritical 或 styled-components 的 SSR 支持
// Next.js App Router
import StyledComponentsRegistry from './lib/registry';
export default function Layout({ children }) {
return (
<html>
<body>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</body>
</html>
);
}
Q2: 样式不生效
tsx
复制代码
// 问题:props 样式未应用
// 解决:确保使用正确的 prop 名称
// ❌ 错误
const Button = styled.button`
background: ${props.variant}; // 缺少 theme 或正确引用
`;
// ✅ 正确
const Button = styled.button`
background: ${props => props.$variant}; // 使用 transient props
background: ${props => props.theme.colors.primary}; // 使用主题
`;
Q3: 性能问题
tsx
复制代码
// 问题:大量动态样式导致性能下降
// 解决:提取静态样式
// ❌ 每次渲染都创建对象
const Bad = styled.div`
padding: 10px;
background: ${props => props.$color}; // 动态
`;
// ✅ 分离静态和动态
const StaticStyles = styled.css`
padding: 10px;
`;
const Good = styled.div`
${StaticStyles}
background: ${props => props.$color};
`;
10.2 Utility-First 常见问题
Q1: 类名过长
tsx
复制代码
// 问题:复杂组件类名太多
// 解决:使用 shortcuts 或组件封装
// tailwind.config.js
module.exports = {
theme: {
extend: {
shortcuts: {
'card': 'bg-white rounded-lg shadow-sm p-6',
'btn-primary': 'px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600',
'input-base': 'w-full px-4 py-2 border border-gray-300 rounded focus:ring-2',
}
}
}
};
// 使用
function Component() {
return (
<div className="card">
<button className="btn-primary">Click</button>
</div>
);
}
Q2: 任意值写法
tsx
复制代码
// 问题:需要使用非预设值
// 解决:使用方括号语法
// 任意颜色
<div className="bg-[#123456]">任意颜色</div>
// 任意数值
<div className="w-[123px] h-[calc(100vh-2rem)]">任意值</div>
// 任意属性
<div className="[--color:red]">CSS 变量</div>
Q3: 深层选择器
tsx
复制代码
// 问题:需要复杂选择器
// 解决:使用 [&] 任意选择器
// & = 当前元素
<div className="[&]:p-4">当前元素</div>
// 嵌套
<div className="[&_p]:font-bold [&_p+&_p]:mt-2">
<p>子元素</p>
</div>
// 伪类组合
<div className="[&:hover>span]:opacity-100">
<span>悬停显示</span>
</div>
11. 总结与选型建议
11.1 方案对比矩阵
| 维度 |
Styled Components |
Emotion |
Tailwind CSS |
UnoCSS |
| 性能 |
⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| 易用性 |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
| 灵活性 |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐ |
| 类型安全 |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
| SSR |
⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| 学习曲线 |
⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
| 生态 |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
| 未来趋势 |
⭐⭐ |
⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
11.2 选型决策
sql
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ 选型决策树 │
└─────────────────────────────────────────────────────────────────────────────┘
开始
│
├─► 项目类型?
│ │
│ ├─► React Native → styled-components ✅
│ │
│ ├─► 新 Web 项目 → Tailwind CSS / UnoCSS ✅
│ │
│ └─► Vue 项目 → Tailwind CSS / UnoCSS ✅
│
├─► 团队背景?
│ │
│ ├─► 熟悉 JS/React → CSS-in-JS 或 Utility-First 都可
│ │
│ └─► 熟悉 CSS → Utility-First 更容易上手
│
├─► 性能要求?
│ │
│ ├─► 极致性能 → Utility-First
│ │
│ └─► 一般性能 → 都可以
│
└─► 项目规模?
│
├─► 小型 → 任意方案
│
├─► 中大型 → Utility-First + 组件库
│
└─► 遗留项目 → 渐进式迁移
11.3 2026 年建议
less
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2026 年技术建议 │
└─────────────────────────────────────────────────────────────────────────────┘
✅ 推荐选择:
│
├── 新 Web 项目 → Tailwind CSS 4.0
│ ├── 理由:性能最佳、生态成熟、Rust 引擎
│ └── 适用:React、Vue、Svelte
│
├── 需要 React Native → styled-components
│ ├── 理由:唯一全面支持 RN 的方案
│ └── 注意:考虑逐步迁移
│
├── 遗留项目 → 渐进式迁移
│ ├── 新组件:Utility-First
│ └── 旧组件:保持不变,逐步重写
│
└── 追求极致性能 → UnoCSS
├── 理由:即时生成、最小 Bundle
└── 适用:大型项目、高性能需求
⚠️ 谨慎选择:
│
├── 新项目使用 CSS-in-JS
│ ├── React 官方不推荐
│ └── 维护风险增加
│
└── 纯 CSS-in-JS 方案
└── 考虑混合方案
🔄 趋势观察:
│
├── Tailwind CSS 4.0 (Rust) 将成为主流
├── UnoCSS 生态快速发展
├── CSS 原生特性 (@layer, CSS 变量) 减少框架依赖
└── 混合方案(Utility-First + 组件库)成为常态
11.4 行动清单
markdown
复制代码
## 开始使用 Utility-First
1. [ ] 安装 Tailwind CSS 或 UnoCSS
2. [ ] 配置设计系统(颜色、间距、字体)
3. [ ] 团队学习基础工具类
4. [ ] 使用 cva/class-variance-authority 管理变体
5. [ ] 使用 tailwind-merge 处理类名合并
## 从 CSS-in-JS 迁移
1. [ ] 评估当前样式复杂度
2. [ ] 新组件使用 Utility-First
3. [ ] 旧组件逐步重写
4. [ ] 移除不必要的 CSS-in-JS 依赖
5. [ ] 统一代码规范
📚 延伸阅读:
CSS、Tailwind CSS、UnoCSS篇结束