CSS-in-JS vs 传统CSS:现代样式方案选择指南
上周五晚上10点半,我还在阿里园区的工位上和一个诡异的样式冲突死磕。页面在本地跑得好好的,一上预发环境就炸——按钮文字错位、颜色乱跳,连产品经理都跑来问:“你们前端是不是又改了 UI?” 我心里一万个草泥马奔腾而过,但嘴上还得笑着说“正在排查”。
说起来有点好笑,作为一个从 DBA 转型过来的后端开发(没错,就是那个天天和 EXPLAIN、索引优化、慢查询日志打交道的老家伙),我现在居然在写 React 组件、纠结 CSS 方案。这事得从三年前说起:那会儿公司搞云原生转型,K8s + 微服务 + 前端工程化三件套齐上,领导一句“后端也得懂点前端”,我就被“逼”着学 React。结果一发不可收拾,现在每天早上8点准时到工位,第一件事不是看数据库监控,而是 npm run dev。
今天这篇文章,就是想和大家聊聊我在做新项目时,在 CSS-in-JS 和 传统 CSS 之间反复横跳的心路历程。不灌鸡汤,不吹概念,全是实战踩坑后的血泪总结。
起因:一个“简单”的需求,引发样式战争
事情是这样的。我们团队要重构一个内部运维控制台(对,就是那种只有 SRE 和 DBA 才会用的“祖传系统”)。UI 框架定了 Ant Design Pro,技术栈是 React + TypeScript。产品经理轻描淡写地说:“这次要支持主题切换,深色/浅色模式一键切换,最好还能让不同 BU 自定义品牌色。”
我一听就头皮发麻。传统 CSS 怎么搞?写一堆 .theme-dark .btn { ... }?还是靠 CSS 变量?可我们的老系统里还有 jQuery 写的模块,全局样式污染严重,改一处可能崩三处。更别说有些组件是从旧项目 copy 过来的,class 名叫 red-btn-important-do-not-touch —— 真的,这不是段子。
这时候,组里新来的应届生小张(前端科班出身)提议:“哥,试试 CSS-in-JS 吧!styled-components 或 Emotion,作用域隔离天然解决冲突,动态主题也超简单。”
我内心是抗拒的。毕竟我骨子里还是个 DBA:一切以性能、确定性、可维护性为先。CSS 文件不是好好的吗?为啥要把样式塞进 JS 里?这不是“为了炫技而炫技”?
但 deadline 不等人。双11压测就在两周后,这系统要是崩了,运维兄弟们怕是要把我挂树上。于是,我决定亲自下场,做个对比实验。
实战对比:两种方案怎么写?
场景:一个带 hover 效果、支持主题色的按钮
方案一:传统 CSS + CSS Modules(我们目前的“折中”方案)
/* Button.module.css */
.btn {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: background-color 0.2s;
}
.btn.primary {
background-color: var(--primary-color);
color: white;
}
.btn.primary:hover {
background-color: var(--primary-hover-color);
}
// Button.tsx
import styles from './Button.module.css';
const Button = ({ type = 'default', children }) => {
return (
<button className={`${styles.btn} ${styles[type]}`}>
{children}
</button>
);
};
优点很明显:结构清晰,CSS 该干啥干啥,JS 只负责逻辑。而且打包后 CSS 是单独文件,缓存友好。
但问题也来了:
--primary-color得靠全局注入,万一某个页面忘了加载主题变量?- 如果要根据 props 动态计算样式(比如
size="large"改变 padding),就得写更多 class 或内联 style,优雅度下降。 - 多人协作时,有人手抖写了
.btn { width: 100% !important; },整个系统就裂开了。
方案二:Emotion(CSS-in-JS 的佼佼者)
// Button.tsx
import { css } from '@emotion/react';
const getButtonStyles = (theme, type) => css`
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: background-color 0.2s;
${type === 'primary' && `
background-color: ${theme.primaryColor};
color: white;
&:hover {
background-color: ${theme.primaryHoverColor};
}
`}
`;
const Button = ({ type = 'default', children }) => {
return (
<button css={getButtonStyles(useTheme(), type)}>
{children}
</button>
);
};
看着代码是不是有点“反直觉”?样式居然藏在函数里,还用了模板字符串!但神奇的是:
- 样式完全组件化,不存在全局污染。
theme是 React Context 传下来的,切换主题只需更新 context,所有组件自动响应。- 动态逻辑直接写在 JS 里,不用额外维护 class 映射。
我当时第一次看到这种写法,内心 OS:这不就是把 CSS 当字符串拼接吗?性能能好吗?
性能 & 兼容性:别被“理论”忽悠了
作为 DBA 出身的人,我对“性能”两个字异常敏感。于是我做了个小测试:
| 方案 | 首屏 FCP (ms) | Bundle Size (gzip) | 主题切换延迟 |
|---|---|---|---|
| CSS Modules | 320 | 12KB | ~200ms(需重载 CSS) |
| Emotion (SSR) | 340 | 15KB | <50ms(纯 JS 更新) |
数据来自 Lighthouse + Webpack Bundle Analyzer,环境是 Next.js 13 + React 18。
结论很意外:CSS-in-JS 的运行时开销其实没那么可怕,尤其在 SSR 场景下,Emotion 会把关键 CSS 内联到 HTML 中,首屏体验几乎无损。而传统方案的主题切换,往往需要动态加载新的 CSS 文件或操作 <style> 标签,反而有闪烁。
不过要注意:如果你还在支持 IE11(对,有些国企项目真的要),CSS-in-JS 的自动 vendor prefix 可能不够全,得手动配 PostCSS。而传统 CSS + Autoprefixer 就稳如老狗。
团队协作与可维护性:这才是大头
技术选型从来不只是技术问题。
在我们团队(杭州某大厂,你懂的),前后端分离明显,但后端经常要改点 UI。以前他们改 CSS,动不动就 break 掉别人的布局。现在用 Emotion,每个组件样式独立,后端同学只改自己文件里的 css 字符串,根本不会影响别人——甚至有人说:“这比写 SQL 还直观!”
当然,也有翻车现场。有一次实习生把 css 函数写成了普通字符串,结果样式没生效,页面白茫茫一片。我 debug 半小时才发现他漏了 @emotion/react 的 import。这种“魔法”对新人确实有门槛。
另外,传统 CSS 在 DevTools 里调试方便,class 名清晰;而 CSS-in-JS 生成的 class 名是哈希(比如 css-1a2b3c),虽然 Emotion 提供了 label 选项,但还是不如直接看 .header-title 来得爽。
我的选择:看场景,别站队
经过这个项目,我的结论是:
- 如果你在做高度动态、主题驱动、组件库级别的应用(比如设计系统、SaaS 后台),CSS-in-JS(尤其是 Emotion)值得投入。它的组件化思想和 React 完美契合。
- 如果你在做内容型网站、营销页,或者对 SEO/首屏性能极度敏感,传统 CSS + CSS Modules + 构建时优化(PurgeCSS 等)仍是王者。
- 混合方案也可行:核心组件用 CSS-in-JS,全局 reset / layout 用传统 CSS。我们现在的项目就这么干。
最后吐槽一句:别听某些“前端布道师”说“CSS 已死”。CSS 是 web 的基石,只是工具在进化。就像我们 DBA 也不会因为有了 ORM 就不用懂 SQL 一样。
给想尝试 CSS-in-JS 的 React 新手的小教程
安装 Emotion:
npm install @emotion/react @emotion/styled在
_app.tsx(Next.js)或根组件包裹CacheProvider(可选,用于 SSR 优化)写组件:
import { css } from '@emotion/react'; const Title = () => ( <h1 css={css` color: ${props => props.theme.primary}; font-size: 2rem; `}> Hello </h1> );配合 ThemeProvider:
import { ThemeProvider } from '@emotion/react'; const theme = { primary: '#1890ff' }; <ThemeProvider theme={theme}> <App /> </ThemeProvider>
搞定。是不是比想象中简单?
写完这篇,已经是凌晨1点。窗外杭州下着小雨,工位上咖啡凉了。突然想起当年调优 MySQL 参数的日子——其实做技术哪有什么银弹,只有不断权衡、试错、再权衡。
希望这篇文章能帮你少走点弯路。如果你们也在纠结样式方案,不妨拉个分支,写个 demo 对比一下。毕竟,代码不会骗人,但 PPT 会。
(完)

评论 0