CSS-in-JS 是一种将样式直接写在 JavaScript 文件中的技术方案,它让每个组件的样式与逻辑、结构真正聚合在一起,实现高内聚的组件封装。React 社区中,styled-components 和 Emotion 是两个最具代表性的库,它们不仅解决了样式作用域冲突,还带来了基于 Props 的动态样式能力。
为什么需要 CSS-in-JS
传统 CSS 在大型项目中存在几个长期痛点:
- 全局污染:所有选择器默认都是全局的,命名冲突难以避免,BEM 等方法论增加了心智负担。
- 样式冗余:随着项目迭代,废弃的样式很难被安全删除,最终导致 CSS 体积不断膨胀。
- 状态关联弱:CSS 无法直接访问组件状态,动态样式需要额外切换 class,逻辑分散。
CSS-in-JS 通过将样式纳入 JavaScript 运行时,从根本上解决了这些问题。你不再需要维护独立的 .css 文件,样式就像普通变量一样被定义和使用,天然享有 JavaScript 的作用域隔离、模块化、类型推断等能力。
styled-components:模板字符串驱动的组件化样式
styled-components 使用 ES6 的标签模板字符串来定义样式化的组件,它的核心理念是“样式即组件”——你创建的不再是普通的 React 组件,而是直接附带样式的视觉元素。
基础用法:
import styled from 'styled-components';
// 创建一个样式化的按钮组件
const Button = styled.button`
background-color: #4f46e5;
color: white;
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
&:hover {
background-color: #4338ca;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
function App() {
return <Button disabled>提交</Button>;
}
编译后,styled-components 会自动生成唯一的类名(如 .sc-fKjTqX),彻底避免样式冲突。
基于 Props 的动态样式:
样式能够直接读取组件的 Props,让样式随数据变化,无需条件性添加 CSS class:
const Button = styled.button`
background-color: ${props => props.variant === 'primary' ? '#4f46e5' : '#e5e7eb'};
color: ${props => props.variant === 'primary' ? 'white' : '#1f2937'};
/* 可传递任意属性 */
font-size: ${props => props.size === 'large' ? '18px' : '14px'};
`;
// 使用
<Button variant="primary" size="large">确认</Button>
<Button variant="secondary">取消</Button>
这种模式让样式逻辑与组件逻辑紧密结合,一个组件所需的全部样式都在同一个文件中,阅读和维护都非常直观。
扩展已有样式:
可以通过 styled() 函数继承另一个 styled 组件的样式,再覆盖或追加新规则:
const PrimaryButton = styled(Button)`
background-color: #4f46e5;
color: white;
`;
主题支持:
通过 ThemeProvider 提供全局主题变量,所有 styled 组件都能通过 props.theme 访问,实现一键换肤:
import { ThemeProvider } from 'styled-components';
const theme = {
primary: '#4f46e5',
secondary: '#e5e7eb',
};
function App() {
return (
<ThemeProvider theme={theme}>
<Button>主题按钮</Button>
</ThemeProvider>
);
}
Emotion:更高性能的 CSS-in-JS 方案
Emotion 的设计目标是在保留 styled-components 开发体验的同时,提供更高效的运行时和更灵活的 API。它主要提供两种写法:styled API(与 styled-components 类似)和 css prop(直接在内联中使用样式对象)。
styled API 模式:
import styled from '@emotion/styled';
const Card = styled.div`
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
`;
写法和 styled-components 几乎一致,迁移成本极低。
css prop 模式:
这是 Emotion 独特的优势,它允许你在组件上直接使用 css 属性编写样式,无需额外创建 styled 组件:
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
function Alert({ type, children }) {
return (
<div
css={css`
padding: 12px;
border-radius: 4px;
background-color: ${type === 'error' ? '#fee2e2' : '#dbeafe'};
color: ${type === 'error' ? '#991b1b' : '#1e3a8a'};
`}
>
{children}
</div>
);
}
注意需要在文件顶部添加 /* @jsxImportSource @emotion/react / 编译指示,以启用 JSX 编译为 Emotion 的 jsx 函数。
组合样式对象:
Emotion 的 css 函数返回一个样式对象,可以自由组合:
const base = css`
padding: 8px 16px;
border-radius: 4px;
`;
const primary = css`
background: blue;
color: white;
`;
function Button({ variant, children }) {
return (
<button css={[base, variant === 'primary' && primary]}>
{children}
</button>
);
}
这种方式比模板字符串的动态插值更可控,而且可以利用数组扁平化合并样式,方便复用。
CSS-in-JS 的核心优势
- 作用域隔离:自动生成唯一类名,天然没有冲突,可放心使用通用短名(如
container、title)。 - 组件内聚性:样式与逻辑、结构同文件,删除组件时样式随之移除,不会留下死代码。
- 动态样式能力:直接使用 JavaScript 变量、Props 和状态,无需预处理器函数,开发体验流畅。
- 主题与设计系统:提供
ThemeProvider和类型推导(结合 TypeScript),轻松统一管理视觉变量。 - 服务端渲染友好:两个库都支持 SSR,服务端提取关键样式,避免首屏闪烁。
必须了解的性能问题
CSS-in-JS 并非零成本,它的运行时开销主要集中在两个方面:
1. 运行时样式注入
每次组件渲染时,Emotion 和 styled-components 都需要将 CSS 字符串序列化并插入到 <head> 的 <style> 标签中。在组件大量渲染(如长列表)或高频更新(如拖拽、动画)时,会产生可感知的性能瓶颈。
- styled-components 会在首次使用某个 styled 组件时将样式注入全局样式表,后续渲染基本无额外注入,但首次解析和插入有成本。
- Emotion 的
cssprop 在每次执行时都会序列化样式,如果样式依赖动态 Props 且频繁变化,开销会更明显。
2. 序列化与计算开销
模板字符串解析为 CSS 字符串需要拼接和插值计算,对于简单样式影响不大,但大量动态样式会累积。Emotion 在 v11 后引入了优化(如 @emotion/react 的 css 函数使用缓存),但仍然需要消耗 JavaScript 线程。
3. 包体积
styled-components 打包后约 12–15KB gziped,Emotion 类似。与原子化 CSS 如 Tailwind(最终 CSS 通常更小)相比,初期加载成本稍高。
实际项目中的缓解策略:
- 静态样式提取:对于不依赖 Props 的样式,使用
@emotion/babel-plugin或 styled-components 的 Babel 插件静态提取,在编译时生成 CSS,减少运行时开销。 - 避免在高频更新组件中使用:动画、拖拽排序等场景,优先使用 CSS 原生的
transition/animation或直接使用行内 style 对象(React 的styleprop 无序列化开销)。 - 合理拆分:不要在一个组件中定义数百行的 styled 组件,过大的模板字符串会拖慢首屏解析。
- 评估项目规模:中小型项目(几百个组件以下)CSS-in-JS 的性能影响微乎其微;大型高流量产品可结合使用 CSS Modules 或考虑零运行时原子化方案。
如何选择:styled-components vs Emotion
| 特性 | styled-components | Emotion |
|------|-------------------|---------|
| API 稳定性 | 成熟稳定,社区广泛 | 灵活多样,迭代积极 |
| 包体积 | 稍大(约 15KB) | 稍小(约 12KB) |
| 性能 | 插入优化,较高效 | css prop 灵活但开销略高 |
| TypeScript 支持 | 良好,需配合 @types | 内置类型,推断更自然 |
| 迁移成本 | 从旧版无缝过渡 | 与 styled-components 写法相似 |
| 文档与社区 | 丰富教程和示例 | 文档清晰,V5 后用户增长快 |
建议:
- 如果你偏爱稳定的 API 和最广泛的社区资源,选择 styled-components。
- 如果你需要更灵活的组合样式(尤其是
cssprop)和更好的打包优化,选择 Emotion。 - 两者可以共存,但为保持一致性,团队最好统一选择其一。
总结
CSS-in-JS 将样式管理拉入 JavaScript 生态系统,让组件真正实现结构、行为与样式的高内聚。styled-components 提供了直观的模板字符串组件化写法,Emotion 则在此基础上增加了 css prop 等灵活模式。虽然存在运行时开销,但在大多数业务场景下完全可以接受,通过静态提取和合理优化,可以平衡开发体验与性能。理解它们的原理和局限,能帮助你在项目中做出合适的技术决策。