用 CSS-in-JS 还是传统 CSS?我在真实项目中的一次抉择与思考
开篇:为什么我会想写这篇文章?

我是一名有五年前端开发经验的工程师,从 jQuery 到 React,从 Bootstrap 到 Tailwind CSS,这些年一直在和样式打交道。但真正让我对“写样式的最佳方式”产生思考,还是源于去年参与的一个大型企业级管理系统重构项目。
那是一个典型的中后台系统,界面复杂、组件繁多,而且需要高度可维护性。我们原本是使用传统 CSS + SCSS 的方式来管理样式,但在迭代过程中遇到了不少困扰:
- 样式冲突频发(比如 class 名重复)
- 随着组件越来越多,维护成本飙升
- 动态样式需求增多(根据状态改变颜色、布局等)
于是我们开始考虑引入 CSS-in-JS 方案,最终选择了 emotion。这个决策过程让我收获了很多经验,也让我更深入地理解了现代前端中样式的管理和演进。
今天我想分享一下这次实战经历,希望能帮助你更好地判断在什么场景下使用哪种样式方案。
问题描述:传统 CSS 的痛点浮现


先简单介绍一下那个项目的背景:
这是一个 B2B 类型的企业管理系统,主要功能包括订单管理、用户权限、报表展示、审批流程等,采用的是 React + TypeScript 的技术栈,UI 框架是基于 Ant Design 封装的一套内部组件库。
初期结构
我们初期采用的是传统的 CSS 写法,每个组件目录下都有一个 .scss 文件,通过 import styles from './xxx.module.scss' 的方式导入使用。看起来很标准,也很合理。
但随着项目体量增长,一些问题逐渐暴露出来:
- 样式冲突:虽然用了
module.scss,但有时候多个组件之间仍会出现 class 名混淆的问题,特别是在嵌套较深或团队协作时。 - 样式可维护性差:某些组件样式文件长达几百行,修改一处往往牵一发动全身。
- 动态样式的处理麻烦:比如按钮的 loading 状态、颜色随主题变化等,传统 CSS 很难做到灵活传参,只能靠一堆条件判断拼字符串。
- 样式复用困难:设计上有很多通用样式,但每次都需要手动复制粘贴,缺乏统一抽象机制。
有一次我们在上线前发现某个表格组件的 tr:hover 样式被其他地方无意改写了,查了整整半天才找到源头,那次事件让我们下定决心:必须换一种更可控的方式。
解决方案:尝试 CSS-in-JS —— emotion

我们对比了几种主流的 CSS-in-JS 方案:styled-components、emotion、linaria 等。
考虑到团队已有 React 经验,以及 emotion 对 TypeScript 和 SSR 的良好支持(虽然我们当时没有 SSR,但为了未来扩展性做准备),我们最终选定了 emotion。
为什么选 emotion?
- 支持 styled API,和 styled-components 语法基本一致
- 优秀的 TypeScript 支持
- 可以通过插件实现 CSS 提取构建优化(配合 webpack)
- 社区活跃且文档清晰
实践过程:逐步替换 vs 全量切换?
由于这是一个老项目,不能一刀切更换所有样式方案,我们采取了逐步替换的方式:
- 新建组件一律使用 emotion 编写样式
- 旧组件只在涉及样式变更时重构为 emotion 写法
- 同时保留部分 SCSS 用于全局变量和字体图标等基础资源
这样做的好处是:不影响现有业务进度,又能逐步验证新方案的可行性。
关键代码示例
下面是一个简单的组件样式改造前后的对比:
原始 SCSS 写法(component/Table/index.js):
import React from 'react';
import styles from './table.module.scss';
const Table = ({ data }) => {
return (
<table className={styles.table}>
<thead>
<tr className={styles.headerRow}>
<th>姓名</th>
<th>年龄</th>
</tr>
</thead>
<tbody>
{data.map((item) => (
<tr key={item.id} className={styles.row}>
<td>{item.name}</td>
<td>{item.age}</td>
</tr>
))}
</tbody>
</table>
);
};
// component/Table/table.module.scss
.table {
width: 100%;
border-collapse: collapse;
.headerRow {
background-color: #f5f5f5;
}
.row:hover {
background-color: #fafafa;
}
}
使用 emotion 后的写法:
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import React from 'react';
const tableStyle = css`
width: 100%;
border-collapse: collapse;
`;
const headerRowStyle = css`
background-color: #f5f5f5;
`;
const rowStyle = (hoverBgColor) => css`
&:hover {
background-color: ${hoverBgColor};
}
`;
const Table = ({ data, hoverBgColor = '#fafafa' }) => {
return (
<table css={tableStyle}>
<thead>
<tr css={headerRowStyle}>
<th>姓名</th>
<th>年龄</th>
</tr>
</thead>
<tbody>
{data.map((item) => (
<tr key={item.id} css={rowStyle(hoverBgColor)}>
<td>{item.name}</td>
<td>{item.age}</td>
</tr>
))}
</tbody>
</table>
);
};
export default Table;
可以看到几点优势:
- 样式直接定义在 JS 中,无需跳转到 SCSS 文件查看
- 动态传参更自然(hoverBgColor 可配置)
- 组件即样式容器,逻辑与表现紧密关联,方便封装和复用
踩坑经验:CSS-in-JS 并非完美解决方案
虽然整体体验不错,但在实施过程中我们也踩了一些坑,这些教训值得分享。
1. 性能问题:频繁创建样式导致性能下降
刚开始我们习惯把样式写在组件内部,比如:
const Button = ({ color = 'blue' }) => {
const buttonStyle = css`
background: ${color};
`;
return <button css={buttonStyle}>点击</button>;
};
这样做确实很方便,但也带来了一个潜在问题:每次渲染都会重新计算 css 字符串并注入到 DOM 中,影响性能。
特别是像表格组件中渲染大量单元格时,很容易造成卡顿。
解决方法:
- 将静态样式提到组件外定义
- 使用
useMemo缓存带参数的样式对象
const useButtonStyle = (color) => useMemo(() => css`
background: ${color};
`, [color]);
2. 样式覆盖问题:emotion 插入顺序影响样式优先级
CSS 是依赖插入顺序的,而 emotion 默认会将样式插入到 <head> 的最末尾。但我们有些组件需要覆盖 Ant Design 的默认样式。
解决方法:
- 使用
@emotion/babel-plugin插件,启用prepend选项插入样式到<head>最前面 - 或者使用
Global组件定义全局样式
import { Global, css } from '@emotion/react';
<Global
styles={css`
.ant-btn {
font-weight: 600;
}
`}
/>
3. 构建体积大:emotion 默认不提取 CSS
如果我们不做特殊配置,emotion 的样式会以内联方式注入 DOM,这会导致首次加载速度变慢。
解决方法:
安装并配置 @emotion/babel-plugin 和 @emotion/minify-webpack-plugin,启用 CSS 提取:
npm install --save-dev @emotion/babel-plugin @emotion/minify-webpack-plugin
Babel 配置:
{
"plugins": ["@emotion/babel-plugin"]
}
Webpack 配置:
const { MinifyPlugin } = require('@emotion/minify-webpack-plugin');
module.exports = {
plugins: [
new MinifyPlugin(),
]
}
效果总结:重构之后的变化
经过几个月的逐步迁移和调试,我们最终完成整个项目超过 70% 的组件样式重构工作。效果如下:
- 样式冲突明显减少:因为 emotion 的 classname 是自动生成的,几乎不会再出现命名冲突
- 样式可维护性提升:样式和组件逻辑在同一文件中,维护起来更直观
- 动态样式更灵活:可以轻松根据 props 生成不同样式,提升了组件的可配置性和复用能力
- 团队协作更顺畅:新人加入后更容易理解组件结构和样式逻辑
虽然初期学习曲线稍陡,特别是对刚接触 CSS-in-JS 的同学来说,但一旦适应之后开发效率显著提升。
经验分享:如何选择 CSS-in-JS 或传统 CSS?

经过这次项目实践,我对两种方案有了更明确的认知。以下是我给前端开发者的建议:
✅ 推荐使用 CSS-in-JS 的场景:
- 组件化程度高、组件数量多的项目(如中后台系统)
- 需要动态控制样式,比如主题色、状态变化样式等
- 团队有一定的工程化能力,能够承担一定的构建成本
- 项目有长期维护计划,希望避免样式混乱
❌ 不太推荐使用 CSS-in-JS 的场景:
- 静态页面居多、交互简单的网站(如企业官网)
- 强调 SEO 或 SSR 首屏速度(未做 CSS 提取)
- 团队对构建工具链不够熟悉,容易出错
- 项目体量较小,追求轻量快速启动
当然,这只是一个大致方向,具体情况还需要结合团队和技术栈综合评估。
结语:样式只是手段,解决问题才是目的
最后我想说,无论是 CSS-in-JS 还是传统 CSS,都只是我们表达 UI 的工具。没有银弹,也没有绝对的好坏之分。
作为开发者,我们要保持开放的心态,理解每种方案的优势和适用场景,并在实践中不断调整自己的认知。
在这个项目之后,我也学会了更理性地看待技术选型,不再迷信“流行趋势”,而是更多地去思考“是否真的适合我们的项目和团队”。
如果你也在纠结应该用哪种方式写样式,不妨动手做个小型 PoC,亲自感受一下两者的差异。毕竟,只有在真实的项目中,才能体会到技术背后的温度。
如果这篇文章对你有帮助,欢迎点赞、收藏或留言交流~

评论 0