继TailWindCss和UnoCss后的CSS-in-JS vs Utility-First 深度对比

28 阅读11分钟

CSS-in-JS vs Utility-First 深度对比

完整技术指南:从架构设计到开发实践的全面对比分析

⚠️ 注意:本文讨论的是整个 CSS 方案生态,包括传统 CSS-in-JS(Styled Components、Emotion)和 Utility-First(Tailwind CSS、UnoCSS),为你选择最适合的样式方案提供全面参考。


目录

  1. 背景与趋势
  2. 核心概念解析
  3. Styled Components 深度指南
  4. Emotion 深度指南
  5. 架构设计深度对比
  6. 性能基准测试
  7. 开发体验详解
  8. 实战案例
  9. 迁移与混用策略
  10. 常见问题与解决方案
  11. 总结与选型建议

1. 背景与趋势

1.1 CSS 方案的演进历程

┌─────────────────────────────────────────────────────────────────────────────┐
                           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 团队的立场变化
// 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 的核心问题
// 问题 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 */
.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 主导?
┌─────────────────────────────────────────────────────────────────────────────┐
│                        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 核心特征
// 特征 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 解决的问题
问题传统 CSSCSS-in-JS
全局污染需要 BEM 命名自动作用域
样式冲突难以追踪唯一哈希
动态样式需要模板字符串原生支持
死代码难以移除摇树优化

2.2 什么是 Utility-First?

Utility-First 是一种使用大量单一功能类(Utility Classes)组合构建界面的方法。

2.2.1 核心特征
// 特征 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 解决的问题
问题传统 CSSUtility-First
类名命名需要思考名称类名即样式
响应式手动写 media query前缀变体
设计一致性需要规范文档内置设计系统
样式复用需要 CSS 组合类名组合

3. Styled Components 深度指南

3.1 基础入门

3.1.1 安装与配置
# 安装
npm install styled-components
# 或
yarn add styled-components

# TypeScript 类型
npm install -D @types/styled-components
3.1.2 第一个组件
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)
// 基础按钮
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)
// 接收 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)
// 使用 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 配置
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
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 全局样式

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 伪类与伪元素
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 嵌套选择器
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 动画与关键帧

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 媒体查询

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 元素
// 支持所有 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
// 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 转发
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 中的使用
// 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+)
// 需要使用 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 安装
# 完整安装(推荐)
npm install @emotion/react @emotion/styled

# 仅核心
npm install @emotion/react

# 僅 styled API
npm install @emotion/styled
4.1.2 三种使用方式
// 方式 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 基础使用
/** @jsxImportSource @emotion/react */

function BasicExample() {
  return (
    <div
      css={{
        padding: '20px',
        backgroundColor: '#f0f0f0',
        borderRadius: '8px'
      }}
    >
      Content
    </div>
  );
}
4.2.2 嵌套与选择器
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 动画
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 基础语法
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
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 继承与扩展
// 基础样式
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 标签
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 条件样式
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
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 使用主题
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 全局样式

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 静态样式提取
// ❌ 性能问题:每次渲染都创建新对象
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 缓存
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 支持

4.8.1 extractCritical
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
// 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 渲染流程对比

┌─────────────────────────────────────────────────────────────────────────────┐
│                          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 ComponentsEmotionTailwind CSS
隔离方式哈希类名哈希类名原子类名
全局污染
动态样式运行时生成运行时生成类名组合
清理机制组件卸载时组件卸载时无需清理
SSR 兼容需要配置需要配置原生支持

5.3 动态样式能力对比

场景Styled ComponentsEmotionTailwind CSS
props 驱动✅ 原生支持✅ 原生支持⚠️ 需条件类名
主题系统✅ 完整✅ 完整✅ 完整
CSS 变量✅ 支持✅ 支持✅ 支持
计算样式✅ 支持✅ 支持⚠️ 受限

6. 性能基准测试

6.1 测试场景

测试项目:
  组件数量: 500
  样式规则: 平均 15 条/组件
  动态样式: 30% 组件有 props 样式
  测试框架: React 18 + Vite 5
  渲染次数: 1000 次状态更新

6.2 开发环境性能

首次加载时间
┌─────────────────────────────────────────────────────────────────────────────┐
│                         首次加载时间 (ms)                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  @emotion/react                                                           │
│  ████████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░  165ms  │
│                                                                            │
│  @emotion/styled                                                         │
│  ██████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░  155ms  │
│                                                                            │
│  styled-components                                                        │
│  ██████████████████████████████████████████████░░░░░░░░░░░░░░░░░░  185ms  │
│                                                                            │
│  Tailwind CSS                                                            │
│  ████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  85ms   │
│                                                                            │
│  UnoCSS                                                                  │
│  ████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  65ms   │
│                                                                            │
└─────────────────────────────────────────────────────────────────────────────┘
内存占用
┌─────────────────────────────────────────────────────────────────────────────┐
│                         运行时内存占用 (MB)                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  styled-components                                                        │
│  ████████████████████████████████████████████████████████░░░░░  52MB     │
│                                                                            │
│  @emotion/react                                                           │
│  ██████████████████████████████████████████████░░░░░░░░░░░░░░░  42MB     │
│                                                                            │
│  @emotion/styled                                                          │
│  ████████████████████████████████████████████░░░░░░░░░░░░░░░░░░  38MB    │
│                                                                            │
│  Tailwind CSS                                                             │
│  ██████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  18MB     │
│                                                                            │
│  UnoCSS                                                                  │
│  ███████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  14MB    │
│                                                                            │
└─────────────────────────────────────────────────────────────────────────────┘

6.3 状态更新性能

1000 次渲染耗时
┌─────────────────────────────────────────────────────────────────────────────┐
│                         渲染耗时 (ms)                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  styled-components                                                        │
│  ██████████████████████████████████████████████████████████████  380ms    │
│                                                                            │
│  @emotion/react                                                           │
│  ██████████████████████████████████████████████████████████░░░░░░░  345ms   │
│                                                                            │
│  @emotion/styled                                                          │
│  ████████████████████████████████████████████████████████░░░░░░░░  320ms    │
│                                                                            │
│  Tailwind CSS                                                             │
│  ████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░  145ms    │
│                                                                            │
│  UnoCSS                                                                  │
│  ██████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  120ms    │
│                                                                            │
└─────────────────────────────────────────────────────────────────────────────┘

6.4 生产构建对比

Bundle 大小
┌─────────────────────────────────────────────────────────────────────────────┐
│                         JS Bundle 增加 (KB)                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  styled-components                                                        │
│  ████████████████████████████████████████████████████████░░░░  +32KB     │
│                                                                            │
│  @emotion/react                                                           │
│  ██████████████████████████████████████████████░░░░░░░░░░░░░░░░  +18KB    │
│                                                                            │
│  @emotion/styled                                                          │
│  █████████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░  +14KB    │
│                                                                            │
│  Tailwind CSS                                                             │
│  ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   +2KB     │
│                                                                            │
│  UnoCSS                                                                  │
│  ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   +1KB     │
│                                                                            │
└─────────────────────────────────────────────────────────────────────────────┘
CSS 输出大小
┌─────────────────────────────────────────────────────────────────────────────┐
│                         CSS 输出大小 (KB, 500 组件)                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  styled-components                                                        │
│  ██████████████████████████████████████████████████████████████  156KB   │
│                                                                            │
│  @emotion/react                                                           │
│  ██████████████████████████████████████████████████████████░░░░░░░░  138KB  │
│                                                                            │
│  @emotion/styled                                                          │
│  ████████████████████████████████████████████████████████░░░░░░░░░░  128KB  │
│                                                                            │
│  Tailwind CSS (JIT)                                                       │
│  ████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░   85KB   │
│                                                                            │
│  UnoCSS                                                                   │
│  ██████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   52KB   │
│                                                                            │
└─────────────────────────────────────────────────────────────────────────────┘

7. 开发体验详解

7.1 代码组织对比

CSS-in-JS 模式
// 优点:样式与组件共存,易于查找
// 缺点:组件文件可能变长

// 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 模式
// 优点: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(完整类型推断)
// 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(类型辅助)
// 需要使用辅助库
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 重构体验对比

样式变更场景
场景:将所有主要按钮从蓝色改为绿色

CSS-in-JS:
├── 需要逐个文件修改
├── 搜索: styled.button`background: blue
├── 替换: background: green
└── 风险: 可能误改其他样式

Utility-First:
├── 修改 tailwind.config.js
├── theme: { colors: { primary: 'green-500' } }
└── 自动全局生效

7.4 调试体验对比

浏览器 DevTools
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 实现
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 实现
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 ComponentsTailwind CSS
总行数142 行78 行
组件文件1 个1 个
样式定义内联类名
可读性⭐⭐⭐⭐⭐⭐⭐⭐⭐
可维护性⭐⭐⭐⭐⭐⭐⭐⭐⭐

9. 迁移与混用策略

9.1 从 CSS-in-JS 迁移到 Utility-First

渐进式迁移策略
// 阶段 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:逐步重写旧组件
样式映射表
┌─────────────────────────────────────────────────────────────────────────────┐
                         样式转换对照表                                       
├─────────────────────────────────────┬─────────────────────────────────────┤
 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 混合使用模式

场景:组件库 + 项目样式
// 使用 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 动画
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)
// 问题: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: 样式不生效
// 问题: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: 性能问题
// 问题:大量动态样式导致性能下降
// 解决:提取静态样式

// ❌ 每次渲染都创建对象
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: 类名过长
// 问题:复杂组件类名太多
// 解决:使用 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: 任意值写法
// 问题:需要使用非预设值
// 解决:使用方括号语法

// 任意颜色
<div className="bg-[#123456]">任意颜色</div>

// 任意数值
<div className="w-[123px] h-[calc(100vh-2rem)]">任意值</div>

// 任意属性
<div className="[--color:red]">CSS 变量</div>
Q3: 深层选择器
// 问题:需要复杂选择器
// 解决:使用 [&] 任意选择器

// & = 当前元素
<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 ComponentsEmotionTailwind CSSUnoCSS
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
易用性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
灵活性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
类型安全⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
SSR⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
学习曲线⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
生态⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
未来趋势⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

11.2 选型决策

┌─────────────────────────────────────────────────────────────────────────────┐
│                            选型决策树                                        │
└─────────────────────────────────────────────────────────────────────────────┘

开始
  │
  ├─► 项目类型?
  │    │
  │    ├─► 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 年建议

┌─────────────────────────────────────────────────────────────────────────────┐
│                          2026 年技术建议                                     │
└─────────────────────────────────────────────────────────────────────────────┘

✅ 推荐选择:
│
├── 新 Web 项目 → Tailwind CSS 4.0
│   ├── 理由:性能最佳、生态成熟、Rust 引擎
│   └── 适用:ReactVueSvelte
│
├── 需要 React Nativestyled-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 行动清单

## 开始使用 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篇结束