CSS-in-JS vs 传统CSS:现代样式方案选择指南
当传统 CSS 遇上 React 世界:我的样式方案进化之路

作为一个前端开发工程师,我参与过多个中大型项目的架构设计和开发工作。从后端转向前端的那一年起,我就一直和“样式”这个看似简单实则深不见底的问题打交道。今天想和大家分享一下我在项目实践中遇到的一个经典问题:如何在现代前端框架(尤其是 React)下合理选择 CSS 的组织方式?
这个问题听起来似乎很简单,但在实际开发过程中,特别是在团队协作、可维护性和性能优化方面,往往成为影响项目质量的关键点。
初识困境:一个组件库升级引发的“血案”
事情得从我负责的一次 UI 组件库重构说起。我们当时维护着一个基于 React 的企业级后台管理系统,使用了传统的 CSS + SCSS 模块化方案,并通过 BEM 命名规范来管理类名冲突的问题。
但随着组件数量的增长,我们遇到了几个明显的问题:
- 命名冲突频发:即使我们遵循 BEM 规范,仍然偶尔会在不同页面上出现相同类名造成的样式污染。
- 共享变量难以统一:SCSS 变量虽然可以在项目中共享,但当组件以 npm 包的形式发布时,这些变量无法被使用者继承,导致主题定制困难。
- 调试和定位样式问题耗时变长:浏览器中的 DevTools 看到的是经过 webpack 提取后的 class 名,跟原始代码完全对不上,排查成本剧增。
- CSS 文件体积逐渐失控:由于没有严格的按需加载机制,构建后的 CSS 资源越来越大。
这些问题在一次产品大版本迭代前集中爆发。我们在尝试升级某个核心组件库时,发现引入新组件会导致已有页面样式错乱,最终耗费了整整两天去排查冲突原因——结果是一个第三方组件不小心注入了一个全局 .btn 样式。
那一刻我意识到,是时候重新审视我们的样式解决方案了。
探索与尝试:CSS-in-JS 是否真香?
既然传统 CSS 遇到了瓶颈,我们就把目光转向了近年来非常热门的 CSS-in-JS 方案。像 styled-components、emotion 这类库在社区里口碑不错,号称可以实现“真正意义上的作用域隔离”,还能通过 JS 动态生成样式逻辑。
于是我们决定在一个小型内部项目中做对比实验。
实验背景
目标是一个用户设置页面,包含多个状态驱动样式的交互组件,比如表单项的禁用态、验证错误提示、切换暗黑/亮色模式等。
我们分别用两种方案实现了同样的功能模块:
- 方案 A:继续使用 SCSS + BEM
- 方案 B:改用 emotion(CSS-in-JS)
关键差异点观察
| 维度 | SCSS 方案 | Emotion 方案 |
|---|---|---|
| 类名唯一性 | 完全依赖人为约定,有风险 | 自动命名,无需担心冲突 |
| 条件样式 | 使用条件判断生成类名,搭配 SCSS Mixin 复杂 | 直接通过对象拼接实现,逻辑清晰 |
| 主题变量传递 | 全局注入 SCSS 变量,难以跨包共享 | 可以结合 context API 实现动态主题 |
| 开发体验 | 类名映射 DevTools 不友好 | 浏览器显示有意义的标识符(如 css-abc123) |
| 构建性能 | 单个 CSS 文件体积较大,首屏加载慢 | 内联样式,首屏无额外阻塞资源 |
说实话,这次实验让我有了很大的转变。Emotion 在条件样式上的灵活性让我印象深刻——以前需要用多个类名组合的方式现在可以直接写成三元表达式嵌入 JSX 中,大大提高了组件本身的可读性。
而且更重要的是,我们不再需要花时间制定复杂的命名规则,也不再因为全局污染而提心吊胆。
实践落地:我们为何选择了 Emotion?
在那次实验之后,我们逐步将部分关键业务组件迁移到了 Emotion,并在后续的新项目中全面采用 CSS-in-JS 方案。
我们的迁移策略
为了不影响现有业务,我们采用了“渐进式迁移”的策略:
- 保留原有 SCSS 结构,仅新增组件使用 Emotion
- 封装主题样式上下文,统一样式变量注入
- 编写通用工具函数,兼容旧有 SCSS 变量逻辑
举个小例子:我们有一个高频率使用的按钮组件 BaseButton。之前的 SCSS 是这样写的:
// _base-button.scss
.base-button {
padding: 12px 16px;
border-radius: 4px;
font-size: 14px;
&--primary {
background-color: $brand-color;
color: white;
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
而在 Emotion 中,我们重写为:
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import { useTheme } from 'your-theme-context';
export const BaseButton = ({ variant = 'default', disabled, children }) => {
const theme = useTheme();
const baseStyles = css`
padding: 12px 16px;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s ease;
`;
const variantStyles = {
default: css`
background-color: ${theme.gray[200]};
color: ${theme.text.primary};
`,
primary: css`
background-color: ${theme.colors.brand};
color: white;
`,
};
return (
<button css={[baseStyles, variantStyles[variant], disabled && disabledStyle]}>
{children}
</button>
);
};
const disabledStyle = css`
opacity: 0.6;
pointer-events: none;
`;
是不是更清晰了?特别是对于动态主题的支持,Emotion 几乎无缝对接。我们也借此机会梳理了整个系统的颜色、字体和间距体系。
踩过的坑:别以为 CSS-in-JS 就万能了
尽管 Emotion 给我们带来了不少便利,但也并不是完全没有问题。下面是我踩过的一些坑,希望你能少走弯路。
1. SSR 支持没做好,首次加载样式闪一下
刚开始我们只顾着客户端渲染的体验,忽略了服务端渲染场景。结果上线后用户反馈首页首次打开会有样式闪动的问题。
原因分析:Emotion 默认会在客户端插入 <style> 标签,而服务端渲染的 HTML 并不包含这些样式内容,因此会出现 FOUC(无样式内容闪烁)。
解决方式:
我们后来引入了 @emotion/server 和 renderToString 时的样式提取插件,确保服务端也能输出带有内联样式的 HTML,避免视觉抖动。
// Express SSR 中间件片段
import { extractCritical } from '@emotion/server';
app.get('/', (req, res) => {
const appHtml = ReactDOMServer.renderToString(<App />);
const { html, ids, css } = extractCritical(appHtml);
res.send(`
<!DOCTYPE html>
<html>
<head>
<style data-emotion="css">${css}</style>
</head>
<body>
<div id="root">${html}</div>
<script>window.__EMOTION__ = ${JSON.stringify(ids)}</script>
</body>
</html>
`);
});
2. 性能瓶颈:过度内联导致渲染卡顿
我们曾经在一个大数据列表组件中大量使用 inline styles,每个条目都会根据状态生成不同的样式对象。结果在浏览器中出现了明显的滚动卡顿。
解决思路:
- 首先通过 React Profiler 定位到是 Emotion 的处理拖慢了渲染速度;
- 接着优化了那些频繁变化的样式逻辑,改用类名 + 动态计算的方式;
- 最终拆分出一些通用状态组合,提前生成静态样式对象供复用。
3. 第三方组件库引入样式污染
有些第三方组件会默认注入全局样式,比如 antd、material-ui 等。这会导致我们的局部样式也受到牵连。
对策:
- 优先使用它们提供的自定义主题能力;
- 如果必须修改样式,尽量在组件外包裹一层容器并使用
@emotion/styled创建带 scoped 样式的 wrapper; - 必要时使用 CSS Modules 或 Shadow DOM 做进一步隔离。
收益与体会:我们得到了什么?
自从我们全面拥抱 Emotion 后,有几个显著的变化:
- 团队协作更顺畅了:大家不用再讨论要不要加前缀、怎么命名类名这些琐事,节省了不少沟通成本。
- 组件开发效率提升了:样式即刻可见,逻辑和视图紧密结合,调试起来更加直观。
- 样式维护变得更容易:删除组件时可以直接顺手删掉对应的样式代码,不需要再去查 scss 文件的引用关系。
- 主题系统统一了:我们基于 Context 提供了一套完整的可定制主题系统,甚至支持运行时切换。
当然,这一切的前提是我们合理地使用了 CSS-in-JS,并没有滥用它。我们依然保留了一些 SCSS 的能力来做全局布局、媒体查询和某些复杂动画。
给你的建议:如何选择适合自己的方案?
如果你也在考虑是否要用 CSS-in-JS,这里是我的一些建议:
✅ 适合使用 CSS-in-JS 的情况:
- 正在使用 React 或其他现代前端框架
- 组件复用性强,追求样式隔离
- 需要动态样式或主题定制能力
- 团队人数较多,容易产生样式冲突
- 构建过程已经比较现代化(比如 Webpack/Vite/Babel)
❌ 不太适合 CSS-in-JS 的情况:
- 项目技术栈老旧,尚未引入 JSX 编译流程
- 有大量遗留 CSS 代码,迁移成本过高
- 极度关注 SEO,对首屏性能要求极高
- 纯静态网站,无需 JS 控制样式逻辑
📌 一些小技巧分享:
- 如果你只是想试试看,可以从一个小的 feature 页面开始,不要一开始就搞全量迁移;
- 使用
eslint-plugin-css-in-js插件帮助检查样式写法是否合理; - 使用
@emotion/babel-plugin可以自动给样式标签加上文件位置信息,方便调试; - 对于公共样式,也可以封装成一个
shared-styles.js,减少重复代码; - 配合 Tailwind 的思路,封装一套你自己的 styled utility 函数也很实用。
尾声:不是替代,而是演进
最后我想说的是,CSS-in-JS 并不是要“取代”传统 CSS,而是前端开发不断演进的结果。我们面对的环境越来越复杂,组件越来越多,样式逻辑也越来越丰富。这个时候,用 JavaScript 的方式去组织和管理样式,反而是一种自然的选择。
我也见过有人坚持使用 SASS 或者 PostCSS + CSS Modules 来解决问题,同样可以做得很好。关键是:要根据自己的项目特点、团队规模和技术栈,选择最合适的方式。
无论是传统的 CSS 还是新兴的 CSS-in-JS,都是我们手中的工具。工具本身没有绝对的好坏,只有合适与否。作为一名开发者,最重要的是理解它们背后的工作原理,从而做出理性的选择。
希望这篇文章能给你带来一些启发,在下一次选型时多一份底气和信心。如果还有疑问,欢迎留言交流,咱们一起探讨!
🔚 文章写到这里就结束了。这篇文章基于我个人经历整理而成,如有雷同,纯属巧合 😊

评论 0