CSS-in-JS vs 传统CSS:现代样式方案选择指南
开篇:为什么我们要谈这个话题?

作为前端团队的负责人,我每天都要面对各种技术选型的决策。从代码结构、框架选型,到UI组件库的设计和样式管理,每一个决定都可能影响项目的长期可维护性、团队协作效率,甚至最终用户的体验。
今天想和大家聊聊一个我们经常遇到但又很容易忽视的技术问题:如何管理项目中的样式?
说到样式管理,传统的做法是写 .css 或 .scss 文件,然后在 HTML 或组件中通过 class 引入;而近年来流行的 CSS-in-JS 方案(如 styled-components、emotion 等)则允许我们在 JS 中直接写 CSS,并绑定到 React 组件上。
这个问题看似简单,但在实际项目中,它往往牵一发而动全身:组件复用时样式的冲突、命名混乱带来的维护成本、多人协作中的 style 全局污染……这些“小”问题日积月累,常常演变成项目推进过程中的大坑。
这篇文章将结合我过去几年在几个重要项目中的实战经验,尤其是最近一次大型重构项目的经历,深入探讨:
- 在什么场景下使用传统 CSS 更合适?
- 什么时候应该考虑引入 CSS-in-JS?
- 我们是如何在这两者之间做出选择的?
- 实施过程中踩了哪些坑,又是怎么爬出来的?
希望这篇真实的工作笔记能帮你在自己的项目中做出更理性的判断。
问题描述:风格混乱的组件系统

事情要从一年多前我们启动的一个项目说起。那是一个企业级后台管理系统,原本是基于 Vue.js 构建的单页面应用,随着时间推移,业务不断扩展,前端部分也经历了多次架构升级,最终转向 React + TypeScript + Ant Design 的组合。
问题是,在开发过程中我们发现样式管理越来越难控制。尤其是在引入第三方组件库后,不同的主题覆盖方式、class 命名习惯不统一、全局样式互相干扰等问题频频出现。
举个例子,某次我们引入了一个封装好的业务组件 OrderCard,结果它的样式总是因为某个外部 .card 类被修改而发生改变。排查了半天才发现,另一个模块里也有一个类似结构的 .card 类定义了 padding 和 border,导致两个完全不相关的组件互相影响。
这其实是一个很典型的 CSS 样式污染问题。而且随着项目越来越大,这样的情况越来越多,工程师每次改样式都不得不小心翼翼地检查 class 名称,生怕误伤其它组件。
更糟糕的是,我们尝试了一些 BEM 规范来组织 CSS 结构,但执行效果参差不齐。新人进来写样式的时候,还是容易忘记加命名空间,或者 class 写得太长太复杂,反而增加了理解成本。
于是,我们开始思考——是不是该换一种思路来处理样式?
解决方案:寻找更现代化的样式方案

初探 CSS Modules
首先我们尝试的是 CSS Modules,它提供了一种局部作用域的解决方案,利用 Webpack 的配置来自动为每个 class 添加唯一的 hash 后缀,防止全局污染。
比如你写了这样一个 CSS 文件:
/* Button.module.css */
.button {
padding: 8px 16px;
background-color: #007bff;
}
然后在组件中这样使用:
import styles from './Button.module.css';
function Button({ children }) {
return <button className={styles.button}>{children}</button>;
}
Webpack 最终生成的 class 可能是 _button_abc123 这样的格式,极大地减少了 class 冲突的可能性。
这种方式一开始让我们非常满意,确实解决了很多样式冲突的问题。但随着组件增多,我们也逐渐发现了一些问题:
- 每个组件都需要一个独立的 CSS 文件,项目文件数量迅速膨胀。
- 动态样式很难处理,比如根据 props 改变背景颜色时,需要额外的 JavaScript 拼接逻辑。
- 风格一致性难以保持,不同人对 modules 文件的组织方式不一致。
进一步探索:CSS-in-JS 的崛起
后来我们开始接触并尝试使用 CSS-in-JS 技术栈中较为成熟的 styled-components 和 emotion。这两个库在 React 社区中非常流行,尤其是 emotion,支持更多的定制化选项,并且性能也相当不错。
我们先以一个小功能模块为实验对象,逐步迁移到 CSS-in-JS 方式。
迁移后的组件看起来像这样:
import styled from '@emotion/styled';
const StyledCard = styled.div`
padding: 16px;
border-radius: 4px;
background: ${props => props.bgColor || '#fff'};
`;
function OrderCard({ bgColor, children }) {
return (
<StyledCard bgColor={bgColor}>
{children}
</StyledCard>
);
}
看到这段代码你会发现几个关键点:
- 样式是用模板字符串书写的,嵌入变量非常方便。
- 每个组件都有自己的 scope,避免了样式污染。
- 变量可以动态传入(比如上面的
bgColor),无需再手动拼串或引入额外的条件逻辑。
更重要的是,这种写法让组件与样式的关系变得更加紧密,增强了组件封装性和可复用性。
踩坑经验:真实项目中的挑战
尽管 CSS-in-JS 带来了很多好处,但也不是没有代价。我们在这个过程中也遇到了一些比较棘手的问题,记录下来供大家参考。
1. SSR 支持不是默认就有的
我们的项目是服务端渲染(SSR)的,初期直接接入 emotion 的时候遇到了样式丢失的问题。服务端渲染出来的页面在客户端 hydration 之前是没有注入 emotion 插入的 <style> 标签的,这就导致页面短暂显示成没有样式的状态,也就是俗称的“闪屏”。
解决办法是按照 emotion 官方文档 的指引,配合 Webpack 配置,设置 extractStaticStyles 并使用 CacheProvider 来包裹整个 App,从而确保服务端渲染时也能正确插入样式标签。
这部分需要一定的学习成本,但一旦配好,稳定性非常好。
2. Tree Shaking 不如预期理想
刚开始我们担心 CSS-in-JS 会带来臃肿的打包体积。虽然 Emotion 支持按需引入、tree shaking,但如果不注意书写方式,某些公共工具函数会被重复打包进多个 chunk,造成冗余。
经过一番调研和测试,我们采用了 @emotion/babel-plugin 和 @emotion/css 的混合写法,把一些全局样式单独抽离出来,减少重复依赖。同时,在构建流程中启用了 Webpack 的 SplitChunksPlugin 对 common 样式进行合并优化。
3. 命名调试难度增加
还有一个小问题是调试。由于样式都是运行时动态生成的类名,开发阶段看浏览器的 Elements 面板时,会发现一堆 .css-xxxxxx 的类名,不太容易一眼看出哪个组件对应哪段样式。
为了缓解这个问题,我们做了两件事:
- 使用 Emotion 提供的
label注释功能,帮助标识组件来源。 - 在开发环境开启
Emotion DevTools插件,可以查看当前元素绑定的 styled 组件信息。
此外,对于特别重要的组件,我们会加上 displayName 属性辅助识别:
const StyledContainer = styled.div(...);
StyledContainer.displayName = 'StyledContainer';
效果总结:样式可控、协作顺畅
经过几轮迭代后,我们将项目的主要功能模块全部迁移到了 CSS-in-JS 的模式下,整体来看,效果非常明显。
首先是开发效率的提升。由于样式写在组件内部,不需要频繁切换 .js 和 .css 文件,编写和维护变得更加流畅。
其次是样式隔离能力更强。不再有莫名奇妙的 class 冲突,也不用再担心第三方 UI 库的样式覆盖问题。
另外一点是代码的可读性和可维护性大大增强。新来的同事只需要看组件本身就能理解其样式逻辑,不需要再去查找对应的 CSS 文件。
最重要的是,团队内部关于样式的协作变得更加统一。以前讨论 class 名称怎么起、是否使用 BEM、要不要加命名空间这些问题,现在基本都不再困扰我们了。
当然,这一切的前提是我们选择了合适的库,并做好了工程化的配套支持。
经验分享:给你的建议
如果你也在面临类似的选择,不妨参考以下几个角度来评估是否适合你的项目采用 CSS-in-JS。
✅ 推荐使用 CSS-in-JS 的场景:
- 你正在做一个以组件为核心构建的系统(例如设计系统、组件库等)
- 组件样式需要高度动态化(依赖 props 控制外观)
- 团队对 CSS 编写规范缺乏统一认知
- 项目已经存在较多样式污染或命名冲突
- 需要极致的封装性和复用性保障
❌ 更适合传统 CSS 的情况:
- 项目体量较小或页面静态内容居多
- 已有大量历史 CSS 代码,迁移成本过高
- 团队成员对 CSS-in-JS 技术栈接受度较低
- 需要极致的加载性能优化(如 SEO 首屏性能要求极高)
🧠 一些实用建议:
不要一刀切!
我们最终采取的是混合策略:核心业务组件使用 CSS-in-JS,基础布局和全局样式仍然保留在 SCSS 中,并通过 PostCSS 处理。注重工程化支持 尤其是对于 SSR、TypeScript 支持、样式提取、调试工具等方面,提前规划好技术路线图和文档引导。
制定样式书写规范 即使用了 CSS-in-JS,也不能放任自由发挥。我们制定了团队的样式书写指南,包括命名习惯、变量使用、断点定义方式等。
适当封装通用样式逻辑 比如封装一个
PrimaryButton时,可以在 styled 组件基础上抽象出一个 base style,便于复用和统一。重视样式性能优化 比如 emotion 默认是用
insertRule插入样式的,但在大量动态样式的情况下,也可以考虑启用prepend或使用cacheProvider自定义注入逻辑。
结语:没有银弹,只有权衡
回过头来看,这次样式方案的调整并不是一蹴而就的过程,而是通过不断地试错、验证、反馈,才找到了最适合我们团队和项目的平衡点。
CSS-in-JS 给我们带来了更好的组件封装能力和样式隔离机制,但它也伴随着一定的学习曲线和实现成本。
传统 CSS 虽然历史悠久,但在现代前端工程中依然有着不可替代的位置,尤其在性能、兼容性、易调试方面。
因此,我想说的是:没有“最好的”样式方案,只有“最适合”的权衡选择。
希望这篇文章能给你带来一些启发,也欢迎你在评论区分享你所在项目中的样式管理实践。如果还有其他前端技术相关的问题,我们下次再聊!
By 黑客猫的日常 —— 一位热爱技术、热爱分享的一线前端负责人 🐱

评论 0