CSS-in-JS vs 传统CSS:现代样式方案选择指南
大家好,我是某互联网公司的一位前端开发者。在过去的几年里,我们团队负责着一个中大型电商平台的重构与优化工作。这个项目贯穿了从架构设计、技术选型到最终上线的全流程,其中在样式管理这一块,我们也经历了一番权衡和试错。
在这篇文章里,我想以第一人称的身份,结合真实项目经验,来聊一聊“CSS-in-JS 与传统 CSS”的实际对比。不是理论堆砌,而是在真实的业务场景下,面对复杂性、协作效率和性能时所做出的选择。希望这篇文章对你在做类似决策时能有所帮助。
背景介绍:为什么会纠结这个选择?

大概一年前,我们团队启动了一个全新的后台管理系统重构计划。旧系统用了传统的 CSS + SCSS 架构,模块划分清晰但存在一些难以解决的问题:
- 样式冲突频发(比如多个组件用了同样的类名)
- 维护成本高(需要查找并更新全局定义)
- 主题切换支持不够灵活
- 开发动态样式时(如基于 props 改变颜色)很费劲
为了解决这些问题,我们在初期阶段就开始思考是否要引入一种更现代化的样式管理方案。于是,摆在我们面前的有两个主要选项:
- 继续沿用传统 CSS/SCSS,但在结构上做优化(比如 BEM 等命名规范)
- 尝试 CSS-in-JS 方案,例如 styled-components 或 Emotion
我们的目标是找到一个既能满足现有开发习惯、又能提高维护性、性能也不拖后腿的方案。那问题到底出在哪呢?
遇到了哪些挑战?

挑战一:组件复用时的样式污染问题
这是个老生常谈的问题。比如,在旧项目里有个 Button 组件,样式写成 .btn 类。结果后来业务上新需求多了几个变体,每个都在不同地方定义了一段 .btn 的样式,最后谁也搞不清楚哪个优先级更高,哪个是多余的。
这类问题在项目规模扩大之后尤为明显。虽然可以通过增加命名空间或者用 PostCSS 插件做自动命名处理,但这些方式都需要额外的学习成本,同时依然无法彻底避免重名的风险。
挑战二:主题定制与动态样式能力不足
我们有一个需求,希望用户可以在页面上自由切换深色/浅色模式,并根据用户的偏好设置实时生效。这就要求样式要有一定的“可注入性”。
传统 CSS 实现的话,一般会通过预定义变量(SCSS 中的 $theme-color)配合构建流程实现主题切换。但这通常是编译期静态决定的,没法运行时动态改变。
如果硬要用 JS 控制 <style> 标签动态注入 CSS 变量,也不是不行,但代码会比较丑陋而且容易失控。
挑战三:性能体验压力山大
虽然传统 CSS 在加载时有天然优势(因为它是纯文本且被浏览器原生支持),但我们发现随着项目体积的增长,特别是在使用 Webpack 多页打包的情况下,CSS 文件经常出现重复、冗余甚至乱序的情况。
更关键的是,关键渲染路径中大量的未拆分 CSS 影响了页面首屏的加载速度。虽然可以通过代码拆分工具优化一部分,但也需要投入不少人力去做精细化配置。
解决思路:我们是怎么评估这两个方向的?

为了不凭感觉做决策,我带着团队做了几轮 Demo 和数据测试,分别尝试以下几种组合:
- 传统 SCSS + BEM 命名法 + 动态注入 CSS 变量
- CSS Modules + CSS 变量 + 动态类名控制
- 使用
styled-components实现完全组件化样式 - 使用
Emotion替代 styled-components 进行优化比对
我们从以下几个维度进行评估:
| 评估项 | 传统 CSS | CSS-in-JS |
|---|---|---|
| 组件隔离能力 | 需人为命名,易冲突 | 自动类名生成,强隔离 |
| 动态样式支持 | 较差,需手动拼接 | 直接传 props 即可 |
| 主题自定义能力 | 编译期静态 | 运行时动态注入 |
| 构建体积 | 小(纯 CSS) | JS 引入后略大 |
| 加载性能 | 更优,尤其首次加载 | 初次渲染稍慢,但可优化 |
| 开发体验 | 熟悉度高,调试简单 | 新概念多,学习曲线陡 |
最终我们选择了 “Emotion + CSS Modules” 的混合模式
这里稍微解释一下,“混合模式”是我们当时的一个折中选择,核心做法是:
- 对于大部分 UI 组件库(如 Button, Modal 等),使用 Emotion 来书写 CSS-in-JS 样式,享受其良好的封装性和动态能力
- 对于基础样式、字体、图标等公共资源,仍然保留 SCSS 并通过 CSS Modules 引入
- 对部分高频交互模块(如表格列宽调整、折叠面板)使用内联样式 + Emotion 动态生成
- 使用
@emotion/cache优化样式插入顺序,避免样式抖动
这种“按需混合”的策略既保留了 CSS 的原有优势,又吸收了 CSS-in-JS 的灵活性。
实施过程:怎么落地这套方案?


第一步:搭建统一的组件样式基础设施
我们在项目中建立了一个 styles 包,里面分为两个部分:
/styles
├── base/
│ └── global.scss // 公共样式 & 字号、颜色变量
├── components/
│ └── button.tsx // Emotion 写法的组件样式
└── theme/
└── index.tsx // 主题上下文提供器
global.scss 依旧使用 SCSS 预处理器定义了常用变量,然后导出为 JS 可识别的形式,供其他文件引用。
// theme/index.tsx
import * as colors from './colors';
export const Theme = {
colors,
};
第二步:逐步替换组件为 Emotion 写法
对于原有的组件,我们采取“渐进式迁移”,即每次改动某个组件时,就顺便把它改为 Emotion 方式。这样不会一次性重构带来风险。
举个例子,原本的 Button 是 SCSS + classnames 的方式:
// 原始写法
const Button: FC<ButtonProps> = ({ variant }) => (
<button className={cx('btn', `btn--${variant}`)} />
);
改造后变成:
// Emotion 写法
const StyledButton = styled.button<StyledButtonProps>`
background: ${props => props.theme.colors.primary};
border-radius: ${props => props.radius || '4px'};
`;
const Button: FC<ButtonProps> = ({ variant, radius }) => (
<StyledButton radius={radius}>{children}</StyledButton>
);
这种方式的好处是显而易见的——我们不再需要记住一堆类名规则,而是直接通过组件 API 传递参数控制样式逻辑。
第三步:构建优化 —— 减少 bundle size 与提升渲染性能
Emotion 默认会在运行时动态生成 <style> 标签,这对 SEO 不友好,也会影响首次加载速度。所以我们做了两件事:
- 服务端渲染(SSR)注入样式标签
我们使用 Next.js 框架,在 App 组件中注入 Emotion 提供的 _document.tsx 支持,保证服务端输出的 HTML 里带有完整样式标签,提升首屏体验。
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
const cache = createCache({ key: 'css' });
function MyApp({ Component, pageProps }: AppProps) {
return (
<CacheProvider value={cache}>
<Component {...pageProps} />
</CacheProvider>
);
}
- Webpack 拆分 & Tree Shaking
确保 Emotion 及其依赖被打包进 vendor chunk,避免重复加载。
另外,我们启用了 @emotion/babel-plugin 插件,将动态模板字符串编译为静态 class 名,进一步减少运行时开销。
第四步:主题切换 + 样式调试实践
由于我们支持深色/浅色模式切换,因此我们使用了 React Context 来管理当前主题状态,并将其传入 Emotion 的 ThemeProvider。
const ToggleTheme = () => {
const [mode, setMode] = useState('light');
const theme = useMemo(() => (mode === 'dark' ? darkTheme : lightTheme), [mode]);
return (
<ThemeProvider theme={theme}>
<MyApp />
</ThemeProvider>
);
};
另外,我们也在 DevTools 插件里集成了 eslint-plugin-cssinjs,用于检查样式的编写规范,避免过度嵌套或不合理的写法。
实施效果如何?
经过几个月的实际使用,我们总结出几个显著的优化成果:
✅ 更高的组件封装性和隔离性
Emotion 自动生成唯一的类名,极大减少了样式污染的可能性。即使是多人协作的场景下,也不用担心命名冲突。
✅ 主题切换更加灵活可控
借助 ThemeProvider,我们实现了完整的亮/暗模式切换,还可以根据不同用户角色动态更换配色主题。
✅ 动态样式逻辑更容易维护
比如一个组件根据 props 显示不同的背景色,以前可能需要多个类名叠加,现在只需写成函数式即可:
const Badge = styled.span<{ color?: string }>`
background: ${props => props.color || '#ccc'};
`;
📈 性能方面略有损耗但总体可控
初期我们确实遇到一些性能瓶颈,特别是首次加载时的样式注入延迟问题。但通过 SSR 注入和打包优化,已经基本可以忽略不计。据 Lighthouse 数据显示,FP(First Paint)只增加了 50ms 左右,视觉影响几乎不可察觉。
我的经验和建议
如果你现在正面临类似的决策,我有几个来自实战的小建议送给你:
✅ 没有一劳永逸的技术选型
CSS-in-JS 固然好,但它并不是银弹。尤其是当你的项目体量较大、已有大量 CSS 积累的时候,贸然全面迁移到 CSS-in-JS 可能导致代码库混乱。
我的建议:采用渐进式迁移策略,针对新增功能和关键组件优先应用 CSS-in-JS,老代码保持现状即可。
✅ 注意性能细节,别让样式拖后腿
CSS-in-JS 是通过 JavaScript 创建 <style> 标签实现的,这可能导致:
- 首屏加载时样式还未插入
- 频繁创建样式标签造成性能浪费
解决办法是:
- 启用 SSR 支持
- 用
@emotion/cache手动缓存样式 - 生产环境启用 minify,压缩冗余样式
✅ 把开发体验放在首位
CSS-in-JS 最大的好处之一就是:你可以把样式逻辑像组件一样组合起来,减少心智负担。
比如我们可以写这样一个公共的按钮样式组件,然后在各个子组件中扩展:
const BaseButton = css`
padding: 8px 16px;
border-radius: 4px;
`;
const PrimaryButton = styled.button`
${BaseButton}
background: #3c8dbc;
`;
const DangerButton = styled.button`
${BaseButton}
background: #d9534f;
`;
这种写法不仅简洁,而且容易复用,对新手也很友好。
✅ 尽早制定团队协作规范
无论你用哪种方式,都需要有一套统一的命名和结构规范。否则不管是不是 CSS-in-JS,都可能写出一团糟的样式。
我们制定了简单的样式书写标准:
- 所有组件样式写在
/components目录下的同名文件中 - 每个组件单独导出自己的 styled 组件
- 使用 TypeScript 接口描述 props 类型
- 禁止在 JSX 中直接写 inline styles
这些看似琐碎的规定,其实大大提升了项目的整体可维护性。
最后的感悟:技术是手段,解决问题才是目的
在整个实施过程中,我最大的感受是:技术本身没有好坏之分,只有合不合适。有时候,我们太容易陷入各种“主流”、“先进”、“流行”的讨论中,却忽略了真正重要的东西 —— 用户体验、开发效率和可维护性。
就像这次重构,我们并没有盲目全盘接受某种方案,而是根据实际情况做了一些因地制宜的取舍。事实证明,这种务实的方式反而让我们走得更快、更稳。
所以,如果你在考虑要不要用 CSS-in-JS,不妨先问自己几个问题:
- 当前的项目有多大?
- 是否有大量已存在的 CSS 代码?
- 是否需要强大的动态样式支持?
- 有没有 SSR 支持条件?
- 团队成员是否愿意接受新的学习成本?
答案不同,选择也会不同。但无论如何,记得一句话:“适合你的,才是真正的好方案。”
致谢
感谢阅读到这里的朋友!如果你也有在实际项目中使用 CSS-in-JS 的经验,欢迎在评论区交流。无论是踩过的坑,还是亮眼的收益,都可以一起分享~
我也期待听到更多关于样式管理的前沿想法,尤其是在 Server Components、Web Components 等新趋势背景下,未来我们会如何更好地管理和组织前端样式体系。这将是下一个值得深入探讨的方向。
本文作者目前就职于某头部互联网公司,长期参与大型前端系统的设计与落地,关注工程效能与用户体验优化。

评论 0