CSS-in-JS vs 传统CSS:现代样式方案选择指南
上个月,我在 GitHub 上给一个开源组件库提了个 PR,结果被 maintainer 狠狠教育了一顿:“兄弟,你这 class 命名太随意了,跟 JavaScript 变量混在一起,维护性堪忧啊。” 我一脸懵,心想我这命名明明清晰又语义化,咋就成“随意”了?后来一聊才知道,人家团队全面拥抱 CSS-in-JS,而我还在用 SCSS + BEM 的老派写法。
这事让我反思了好几天。作为一个从 DBA 转型过来的后端开发(对,就是那个整天盯着慢查询、死锁、索引优化的人),我对代码结构和可维护性的执念,其实比很多前端还重。数据库讲究范式、一致性、事务隔离——这些理念迁移到前端工程里,不也该体现在样式的组织方式上吗?
最近在准备跳槽,一边刷 LeetCode,一边疯狂补前端新知识。上周五晚上加班到十点,产品经理突然在钉钉群里甩来一句:“这个按钮 hover 效果要加个微动效,明天上线。” 我差点把咖啡泼到键盘上——我们项目用的是纯 CSS Modules,改个动画得先建个新 class,再 import,还得担心全局污染……那一刻,我真想试试 Emotion 或者 Styled Components。
于是,我决定系统梳理一下:在现代前端工程中,我们到底该怎么选样式方案?
一场关于“样式归属”的哲学争论
说白了,CSS-in-JS 和传统 CSS 的核心分歧,不是语法问题,而是**关注点分离(Separation of Concerns)**的理解差异。
传统派认为:“HTML 负责结构,CSS 负责样式,JS 负责行为”——这是教科书级的分层思想。而 CSS-in-JS 派则反驳:“组件化时代,样式本就是组件逻辑的一部分,强行拆出去反而割裂了封装。”
这话听着耳熟吧?就像当年我们在数据库设计时争论“该不该用 JSON 字段存复杂结构”——关系型信徒说“破坏范式”,NoSQL 爱好者说“灵活才是王道”。
但现实哪有非黑即白?我在去年双11大促前重构支付页面时就深有体会:一个“立即支付”按钮,在不同营销活动中要动态换色、换圆角、甚至换字体粗细。如果用传统 CSS,要么写一堆 button--campaign-a、button--campaign-b,要么在 JS 里动态拼 class 名——后者简直像在 SQL 里拼字符串,随时可能注入漏洞(虽然只是样式混乱)。
而用 Styled Components,直接传 props 就行:
import styled from 'styled-components';
const PayButton = styled.button`
background: ${props => props.themeColor || '#007aff'};
border-radius: ${props => props.rounded ? '24px' : '4px'};
font-weight: ${props => props.bold ? '700' : '400'};
transition: all 0.2s ease;
&:hover {
transform: scale(1.03);
}
`;
// 使用
<PayButton themeColor="#ff6b6b" rounded bold>立即支付</PayButton>
看到没?样式逻辑和组件状态耦合得天衣无缝。这不就是我们 DBA 常说的“数据与业务紧耦合”吗?只不过这里,“数据”是 UI 状态,“业务”是视觉表现。
真实战场:我在项目中的踩坑记录
我们团队目前维护着一个中后台管理系统,技术栈是 React + Ant Design + Less。早期为了快速上线,样式管理相当混乱:全局变量乱用、class 命名随意、媒体查询散落在各处。结果某次迭代,测试同学报了个 Bug:“用户列表页的筛选框在 Safari 上错位了。”
我花了两小时排查,最后发现是因为某个同事在 global.less 里加了:
input {
box-sizing: border-box;
}
但忘了 Safari 对某些 input 类型的默认样式处理不同……这种“隐式副作用”简直是我这种 DBA 出身的人的噩梦——就像有人在生产库偷偷改了默认隔离级别却不告诉你。
于是我们决定引入 CSS Modules 作为过渡方案。效果立竿见影:每个组件的样式自动 scoped,再也不怕全局污染。但问题也随之而来:
- 动态主题切换?得靠 CSS Variables + JS 动态设置
:root,或者用classnames手动拼接。 - 条件样式?写一堆 ternary operator 在 className 里,可读性爆炸。
- 性能?每个组件生成独立 CSS 文件,HTTP 请求增多(虽然后续可以合并,但构建配置变复杂)。
这时候,隔壁组用 Emotion 的同事看不下去了,甩给我一段代码:
import { css } from '@emotion/react';
const getButtonStyle = (props) => css`
padding: ${props.size === 'large' ? '12px 24px' : '8px 16px'};
color: ${props.disabled ? '#ccc' : props.textColor};
cursor: ${props.disabled ? 'not-allowed' : 'pointer'};
`;
function MyButton({ size, disabled, textColor, children }) {
return (
<button
css={getButtonStyle({ size, disabled, textColor })}
>
{children}
</button>
);
}
我试了下,真香!条件逻辑直接写在样式里,不用绕弯子。而且 Emotion 支持 SSR、自动 vendor prefix、关键 CSS 提取——这些在传统 CSS 里得靠 PostCSS 插件链堆出来。
但转头我就被运维大哥警告了:“你们前端包体积又涨了 15%!CDN 流量费用要超预算了!” ——原来 CSS-in-JS 的运行时(runtime)会增加 bundle size。虽然可以通过 @emotion/babel-plugin 做编译时优化,但配置起来比写个 .postcssrc 麻烦多了。
性能、兼容性、调试体验:不能只看表面
我知道很多老派开发者(包括我自己刚开始)会说:“CSS-in-JS 运行时开销大,影响首屏性能。” 但真相是:现代方案已经极大优化了这个问题。
以 Emotion 为例,开启 sourceMap 和 autoLabel 后,你在 DevTools 里看到的 class 名是这样的:
css-1wq8r9a-MyButton
而不是以前那种 aB3cD 的随机字符串。更绝的是,它还能和 React DevTools 联动,直接在组件树里看到样式规则!
至于性能,我在本地做了个简单 benchmark(用 Lighthouse):
| 方案 | FCP (ms) | Bundle Size (KB) | TTI (ms) |
|---|---|---|---|
| Pure CSS + Modules | 820 | 120 | 950 |
| Emotion (runtime) | 860 | 145 | 980 |
| Emotion (compiled) | 830 | 128 | 960 |
差距几乎可以忽略。而且 compiled 模式下,Emotion 会把样式提取成静态 CSS,和传统方案无异。
不过,IE11 用户慎入。虽然有些 CSS-in-JS 库声称支持 IE,但实际用起来各种 polyfill 塞满 webpack config,还不如直接用 Sass + Autoprefixer 省心。
一张对比表,帮你快速决策
结合我最近刷题、面经、GitHub issues 里的讨论,整理了这张实战对比表:
| 维度 | 传统 CSS (SCSS/Less + Modules) | CSS-in-JS (Emotion/Styled Components) |
|---|---|---|
| 学习成本 | 低(前端基本功) | 中(需理解 JS 作用域、闭包) |
| 动态样式 | 靠 CSS Variables / 多 class 切换 | 直接传 props,天然支持 |
| 主题切换 | 需配合 JS 动态修改 :root | 通过 ThemeProvider 全局注入 |
| Tree Shaking | 难(除非用 PurgeCSS) | 自动(未使用的样式不会打包) |
| 调试体验 | DevTools 直接编辑 CSS | 需 source map,但可读性已改善 |
| SSR 支持 | 原生支持 | 需额外配置(如 extractCritical) |
| Bundle Size | 小(纯静态资源) | 中(运行时 ~5-10KB,可编译时消除) |
| TypeScript 支持 | 弱(需 .d.ts 声明) | 强(样式即函数,类型推导自然) |
| 团队协作 | 需严格规范 BEM/命名 | 组件即文档,自包含性强 |
注:数据基于 React 18 + Webpack 5 环境,实测于 2024 年 6 月
如果你团队里有设计师用 Figma,并且要求“像素级还原+动态主题”,那 CSS-in-JS 几乎是必选项。但如果你在维护一个政府项目,用户还在用 IE8(别笑,真有),那就老老实实用原生 CSS 吧。
我的选择:混合架构 + 渐进式演进
经过几轮折腾,我们最终采用了 混合策略:
- 基础组件(Button, Input, Layout)用 Emotion 编写,利用其动态能力和类型安全;
- 页面级样式仍用 CSS Modules,避免过度抽象;
- 全局 reset 和 utility classes(比如
.text-center,.hidden)保留为传统 CSS,通过 CDN 单独加载。
构建配置如下(webpack.config.js 片段):
module.exports = {
module: {
rules: [
// Emotion 编译时优化
{
test: /\.(js|jsx|ts|tsx)$/,
use: [
{
loader: '@emotion/babel-plugin',
options: {
sourceMap: true,
autoLabel: 'dev-only',
labelFormat: '[local]--[filename]',
},
},
],
},
// CSS Modules 用于页面样式
{
test: /\.module\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { modules: true },
},
'less-loader',
],
},
],
},
};
上线两周,零样式相关线上事故。产品经理再也没半夜钉钉我改按钮样式了(感动哭)。
写在最后:代码人生,没有银弹
回想起刚转前端那会儿,我还嘲笑过 CSS-in-JS 是“为了炫技”。现在才明白,工具的选择,本质是对工程复杂度的认知。
就像我们在数据库选型时,不会因为 PostgreSQL 功能强大就抛弃 MySQL——要看业务场景、团队能力、维护成本。
我在 GitHub 上 star 过上百个前端项目,发现一个有趣现象:高 Star 数的 UI 库(如 Material-UI、Chakra UI)几乎全部采用 CSS-in-JS;而工具类库(如 date-fns、axios)则完全不用。这说明什么?当“样式”成为核心产品的一部分时,封装性和动态性就变得至关重要。
所以,别再问“哪个更好”了。问问自己:
- 我的组件需要频繁根据状态改变样式吗?
- 团队能否接受额外的学习曲线?
- 项目的生命周期有多长?是否值得投入架构升级?
上周面试一家独角兽公司,面试官问我:“你怎么看待 CSS-in-JS?” 我没直接回答,反问:“你们的设计系统是动态主题吗?SSR 场景多吗?” 他愣了一下,笑着说:“看来你是真干过活的。”
代码人生,从来不是非此即彼。而是在无数 deadline、线上 bug、产品经理的“小需求”中,找到那个刚刚好的平衡点。
共勉。

评论 0