CSS-in-JS vs 传统CSS:现代样式方案选择指南
上个月,我在公司内部搞了一次技术分享,主题就是“前端样式到底该怎么写”。结果刚讲完,隔壁组的后端同事跑过来问我:“你们前端是不是闲得慌?连写个样式都要搞出十几种方案?”我只能苦笑——这事儿还真不是我们想折腾,而是业务逼的。
我是某上市公司技术中台团队的老油条了,待了三年多,每天一边听着 Lo-fi Beats 写代码,一边琢磨着要不要换个环境。最近半年,我们团队接手了一个新项目:一个面向企业客户的可视化配置平台,要求高复用、强隔离、支持动态主题切换,甚至还要考虑未来和区块链数据看板集成(别问,问就是产品总监看了某白皮书突发奇想)。就在这个节骨眼上,样式方案成了争论焦点——到底是继续用传统的 .css 文件,还是全面拥抱 CSS-in-JS?
说实话,去年双11大促前,我们还因为样式冲突导致某个组件在生产环境“裸奔”了两个小时。当时产品经理在群里@我:“用户说按钮变透明了,还能点吗?”我盯着满屏的 !important 和莫名其妙的全局类名,真的想砸电脑。
起因:一次“主题切换”的灾难
事情的导火索是产品提了个需求:支持多租户动态主题切换,不同客户登录后看到的 UI 颜色、间距、字体统统不一样。而且,这些主题信息是从后端 API 动态拉取的——这意味着我们不能在构建时预设所有样式。
最初,我们尝试用 SCSS + CSS Variables 搞定。想法很美好:
:root {
--primary-color: #007aff;
--border-radius: 4px;
}
.button {
background: var(--primary-color);
border-radius: var(--border-radius);
}
然后在 JS 里动态更新:
document.documentElement.style.setProperty('--primary-color', tenantTheme.primaryColor);
看起来很 clean 对吧?但问题很快暴露:
- IE 不支持 CSS Variables(虽然现在 IE 用户少,但我们的政企客户还在用)
- 复杂组件(比如带 hover、focus、disabled 状态的下拉框)状态样式难以维护
- 更致命的是——样式泄露。某个小组件不小心用了全局类名
.card,结果被另一个团队的.card:hover覆盖了,线上直接炸了。
运维小哥半夜打我电话:“兄弟,监控显示错误率飙升,是不是你下午上线那个‘主题’搞的鬼?”
我:“……大概吧。”
探索 CSS-in-JS:从抗拒到真香
被线上事故教育后,团队决定认真评估 CSS-in-JS。我一开始是抗拒的——毕竟写了十年 CSS,总觉得把样式塞进 JS 里是“邪道”。但现实逼人成长啊。
我们重点试了两个主流库:styled-components 和 Emotion。选它们是因为社区活跃、TypeScript 支持好,而且我们用的 React 技术栈天然契合。
先看一个 Emotion 的例子:
import { css, jsx } from '@emotion/react';
const Button = ({ variant, children }) => (
<button
css={css`
padding: 8px 16px;
border-radius: ${variant === 'rounded' ? '20px' : '4px'};
background: ${theme.primaryColor}; // 来自动态主题上下文
&:hover {
opacity: 0.9;
}
`}
>
{children}
</button>
);
优势立马显现:
- 作用域隔离:每个组件样式自动加了唯一 hash,再也不怕
.button被覆盖 - 动态性极强:props 和 theme 直接注入样式,不用再拼字符串或操作 DOM
- 逻辑与样式共存:条件样式写起来比 SCSS 的
@if清晰多了
但坑也不少。最头疼的是性能问题。有一次我在一个循环渲染的列表里用了 styled-component,结果页面卡成 PPT。Chrome DevTools 一看,每帧都在生成新的 <style> 标签——原来我没用 shouldForwardProp 过滤掉非 DOM 属性,导致每次 props 变化都触发重渲染。
后来学乖了,关键路径上尽量用静态样式,动态部分抽离成独立 hook:
// theme/useDynamicStyle.ts
export const useButtonStyle = (variant: string) => {
return css`
border-radius: ${variant === 'rounded' ? '20px' : '4px'};
// ...其他动态逻辑
`;
};
对比实测:传统 CSS 真的过时了吗?
为了说服保守派同事(以及我自己),我搞了个小 benchmark。场景很简单:渲染 1000 个带 hover 效果的卡片,分别用三种方案:
- 纯 CSS + class 切换
- CSS Modules
- Emotion (with cache)
结果如下(本地 MacBook Pro, Chrome 124):
| 方案 | 首屏 FCP (ms) | 内存占用 (MB) | Bundle Size 增量 |
|---|---|---|---|
| 纯 CSS | 85 | 42 | +0KB |
| g | CSS Modules | 92 | 44 |
| Emotion | 118 | 58 | +18KB |
注:FCP = First Contentful Paint,越低越好
结论很明显:传统 CSS 在性能上依然有优势,尤其是对静态内容。但如果你需要大量运行时动态样式(比如我们的主题系统),CSS-in-JS 的开发体验和维护成本优势就碾压了。
另外提一嘴兼容性:CSS Modules 能完美支持到 IE11(只要配好 postcss-loader),而 Emotion 默认只到 IE11+,且需要额外 polyfill。不过对我们这种 ToB 产品来说,客户浏览器版本可控,倒也不是大问题。
区块链项目的特殊考量
说到区块链,你可能觉得和前端样式八竿子打不着。但我们最近确实在对接一个链上数据看板项目——用户要实时查看 NFT 交易记录,界面元素会根据链上事件动态变色(比如大额交易标红)。
这种场景下,传统 CSS 几乎没法做:
- 无法预知哪些元素需要高亮
- 高亮规则来自智能合约返回的数据
- 用户可能同时看多个链,主题切换频率极高
最后我们用 Emotion + React Context 搞定:
// ThemeProvider 封装
<ThemeProvider theme={dynamicChainTheme}>
<TransactionList transactions={txs} />
</ThemeProvider>
// 组件内直接消费
const TransactionItem = ({ tx }) => {
const isLarge = tx.value > 100;
return (
<div css={{
color: isLarge ? 'red' : 'inherit',
backgroundColor: theme.bgCard
}}>
{tx.hash}
</div>
);
};
要是用传统 CSS,估计得写一堆 data-large="true" + [data-large="true"] { color: red; },维护起来绝对头秃。
我的建议:别站队,看场景
经过这半年的折腾,我对样式方案的看法变了:没有银弹,只有权衡。
什么情况下坚持传统 CSS?
- 项目是内容型网站(博客、文档站)
- 团队有专职 UI 工程师,习惯用 Figma + CSS workflow
- 极致性能要求(比如首屏速度 < 1s)
- 需要支持老旧浏览器(IE11 及以下)
什么情况下上 CSS-in-JS?
- 组件库开发(强隔离刚需)
- 需要运行时动态主题/皮肤
- 项目重度依赖 React/Vue,追求逻辑与样式统一
- 团队愿意接受 slightly larger bundle(一般多 10~30KB)
顺便吐槽一句:有些教程一上来就说“CSS-in-JS 是未来”,结果自己 demo 里连 SSR 都没处理好,hydration 报错满天飞。技术选型不是追新,而是解决问题。
最后:关于跳槽和技术债
写这篇文章的时候,我正在整理简历。三年多来,从抗拒工程化到拥抱现代化方案,踩过的坑比我写的 if-else 还多。CSS-in-JS 并不是终点——最近团队已经在调研 Vanilla Extract 这种编译时方案,试图兼顾零 runtime 和类型安全。
技术中台的日子,就是在“快速交付”和“长期可维护”之间走钢丝。每次大促前,测试同学都会幽幽地说:“这次样式别又崩了吧?” 而我能做的,就是在 deadline 前多喝几杯咖啡,把 z-index 调到 999999(开玩笑的,千万别学)。
如果你也在纠结样式方案,不妨问问自己:我的痛点到底是什么? 是团队协作混乱?性能瓶颈?还是产品经理又提了“动态换肤”这种需求?
搞清楚这点,答案自然就出来了。
P.S. 如果你对区块链前端开发感兴趣,我整理了一份入门教程清单(含 Web3.js + Ethers.js 实战),评论区留言“链上样式”我私你。反正快离职了,资料不留着发霉 :)

评论 0