跳到主要内容

CSS-in-JS 阶段

要点

  • 组件边界天然隔离,props 可驱动样式;动态主题/状态友好。
  • 成本:运行时/SSR 注水体积、构建链复杂;类名可读性差。
  • 适合需要高度动态样式、主题切换、设计系统与组件强绑定的团队。
  • 代表性包:运行时流派(styled-componentsEmotionJSS)、编译期/零运行时流派(vanilla-extractLinariaAstro/Uno + scoped 混合方案)。

优势 / 劣势 / 何时使用

内容
优势组件粒度隔离;props 驱动;可在 JS 里复用逻辑/常量
劣势运行时开销;SSR 注水;类名调试性差;可能影响 HMR 速度
适用需要复杂动态样式、主题即逻辑的设计系统;部分 SSR/CSR 混合场景
不适用对首屏体积极敏感、运行时预算极小的多端/小程序场景

代表性包与用法

  • styled-components(运行时):模板字符串 + props;支持 ThemeProvider 统一主题。
styled-components 主题
import styled, { ThemeProvider } from 'styled-components'

const theme = { primary: '#111827', radius: '12px' }
const Button = styled.button`
padding: 10px 16px;
border-radius: ${({ theme }) => theme.radius};
background: ${({ theme }) => theme.primary};
`

export function Demo() {
return (
<ThemeProvider theme={theme}>
<Button>Dark Button</Button>
</ThemeProvider>
)
}
  • Emotion(运行时 + 编译模式)css prop 与 @emotion/babel-plugin 编译模式减少运行时。
Emotion css prop
import { css } from '@emotion/react'

const card = css({
border: '1px solid #e5e7eb',
borderRadius: 16,
padding: 16,
})

export const Card = () => <section css={card}>Emotion css prop</section>
  • vanilla-extract(零运行时):TypeScript API 生成 CSS,运行时仅使用 className。
vanilla-extract button.css.ts
import { style, createVar } from '@vanilla-extract/css'

export const color = createVar()
export const button = style({
vars: { [color]: '#111827' },
background: color,
borderRadius: '8px',
color: '#fff',
})

运行时 vs 编译期(示意)

运行时注入:在客户端生成 style,首屏注水 + HMR 有额外成本。
编译期生成:静态 CSS 提前产出,JS 只保留 className 映射。

写法与产物对照

  • 运行时(styled-components/Emotion):
    • 写法:模板字符串或对象样式,允许使用 props / theme 计算样式;开发态插入 <style>,生产态可能提取 critical CSS。
    • 产物:JS bundle + 内联 style 标签,类名在运行时生成(sc-abc123),首屏注水与 HMR 需要样式注入开销。
  • 编译期(Linaria/vanilla-extract):
    • 写法:受限的模板字符串或 TS API(css/style),编译器提前求值并生成 .css,JS 中只保留 className 映射。
    • 产物:静态 CSS 文件(或内联 chunk)+ 极薄的 className 映射,运行时不再注入 style,SSR 直接 link CSS。

Linaria 示例(编译期)

源码(编译前):

card.tsx
import { css } from '@linaria/core'

const card = css`
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 16px;
background: white;
transition: box-shadow 150ms ease;

&:hover {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
}
`

export function Card({ children }: { children: React.ReactNode }) {
return <section className={card}>{children}</section>
}

编译产物(示意):

card.linaria.css
.card_h3dj1z {
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 16px;
background: white;
transition: box-shadow 150ms ease;
}
.card_h3dj1z:hover {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
}
card.tsx (compiled excerpt)
import './card.linaria.css'
const card = 'card_h3dj1z'
export function Card({ children }) {
return <section className={card}>{children}</section>
}

特点:CSS 被提前生成,JS 仅保留类名字符串;运行时不再注入 <style>,与传统静态 CSS 链路类似。

示例(styled-components)

import styled from 'styled-components'

const Card = styled.section`
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 16px;
box-shadow: ${({ elevated }) => (elevated ? '0 10px 30px rgba(0,0,0,0.06)' : 'none')};
`

const Button = styled.button`
padding: 10px 16px;
border-radius: 8px;
border: 1px solid #111827;
background: #111827;
color: #fff;
&:hover { background: #0f172a; }
`

export const Demo = () => (
<Card elevated>
<p className="eyebrow">CSS-in-JS</p>
<h2>动态样式,组件边界</h2>
<Button>查看详情</Button>
</Card>
)

常见坑与对策

  • 运行时体积:优先选择编译模式或零运行时方案(如 vanilla-extract),或开启 Babel SWC 优化。
  • SSR 注水:衡量首屏样式注入体积;必要时提取静态样式或开启样式缓存。
  • 类名调试:在开发环境打开 displayName/label;在生产环境启用最小化。
  • HMR 性能:减少过深的动态表达式;拆分组件,降低热更范围。