CSS-in-JS vs 传统CSS:现代样式方案选择指南
上周五晚上十点半,我坐在公司最后一盏还亮着的灯下,盯着一个线上样式的诡异错位——按钮在 Safari 上莫名其妙地多了一层阴影,在 Chrome 却完美无瑕。产品同学已经在群里@了三次:“老板说这个双11主会场必须零视觉 bug。”运维兄弟幽幽来了一句:“你确定不是又用了什么 fancy 的新工具?”
那一刻我真的想砸键盘。
我是老李,杭州一家三线互联网公司的技术负责人。虽然说是“负责人”,但其实团队就七个人,前端俩、后端仨、测试一个、产品经理……算了他不算人。我们公司离网易园区走路十五分钟,阿里西溪园区打车二十块起步。说实话,在这种地方做技术,既没大厂资源,又得顶住业务压力,常常是“既要又要还要”——既要快速迭代,又要稳定上线,还要兼容 IE(别笑,真的还有客户用!)。
最近半年,我们在重构一个核心运营活动平台,正好碰上了样式架构选型的问题。传统 CSS 还是 CSS-in-JS?这个问题看似简单,实则牵一发动全身。今天就想和大家掏心窝子聊聊我们的踩坑经历、权衡思路,以及最终怎么在“理想”和“现实”之间找到那条缝。
起因:一次“优雅”的样式崩坏
事情要从去年双11说起。当时我们用的是最传统的 CSS + BEM 命名规范,配合 Webpack 的 MiniCssExtractPlugin 打包。一切看起来岁月静好,直到某天前端小王为了“提升开发体验”,偷偷在某个新模块里引入了 styled-components。
结果呢?两个组件同时用了 .button--primary,一个来自传统 CSS,一个来自 styled-components 动态生成的 class(比如 sc-1a2b3c4-0 kLmNoP),权重打架,样式覆盖混乱。更糟的是,因为 styled-components 把样式注入到 <head> 的 <style> 标签里,而传统 CSS 是外链的,加载顺序不可控,导致某些页面在首屏渲染时出现“样式闪烁”(FOUC)。
线上报错不多,但 UI 审查时被设计小姐姐追着问:“为什么同一个按钮,有的圆角有的方角?”我只能苦笑:“因为我们有两个宇宙。”
这件事之后,团队开了个复盘会。有人提议全面拥抱 CSS-in-JS,有人说干脆回归纯 CSS + CSS Modules。吵到最后,我说:“别争了,咱们做个实验——拿真实业务场景测一测。”
真刀真枪:我们做了什么对比?
我们选了三个维度来评估:开发体验、构建性能、运行时表现。每个方案都用在了一个真实的活动页模块上(就是那种三天出稿、五天上线、七天改十版的那种“敏捷”项目)。
方案一:传统 CSS + PostCSS + BEM
这是我们原来的主力方案。优点大家都懂:成熟、稳定、工具链丰富。PostCSS 插件生态强大,Autoprefixer 自动补全前缀,cssnano 压缩体积。BEM 虽然写起来啰嗦,但至少不会命名冲突。
/* activity-card.css */
.activity-card {
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.activity-card__title {
font-size: 18px;
color: #333;
}
痛点也很明显:
- 全局作用域,哪怕用了 BEM,也不敢保证没人手滑写了个
.title; - 无法动态传参(比如根据主题色切换),要么写一堆 modifier class,要么靠 JS 操作 classList;
- Tree-shaking 几乎无效,哪怕你只用了一个组件,整个 CSS 文件都会被打包进去。
方案二:CSS Modules
这是传统 CSS 的“安全升级版”。通过 Webpack 的 css-loader 启用 modules,自动 hash class 名,彻底解决命名冲突。
// Button.jsx
import styles from './Button.module.css';
export default () => <button className={styles.primary}>Click</button>;
生成的 HTML 会是:
<button class="Button_primary__aB3cD">Click</button>
优点:
- 零冲突,作用域隔离;
- 支持组合(composes),可以复用基础样式;
- 构建时处理,运行时无额外开销。
缺点:
- 动态样式依然难搞。比如“根据用户等级显示不同边框颜色”,你得预先定义好
.level-1,.level-2... 然后在 JS 里拼字符串; - 调试时 DevTools 里的 class 名是 hash,虽然可以用
[name]__[local]配置可读,但终究不如直接看类名直观; - 和设计师协作时,他们给的 Sketch/Figma 样式名对不上,沟通成本高。
方案三:CSS-in-JS(以 Emotion 为例)
我们试了 styled-components 和 @emotion/react,最终选了 Emotion,因为它支持 SSR 更友好,且有 css prop 这种灵活用法。
import { css } from '@emotion/react';
const buttonStyle = (theme) => css`
border-radius: 4px;
background: ${theme.primaryColor};
&:hover {
opacity: 0.9;
}
`;
export default ({ level }) => (
<button
css={buttonStyle}
style={{ borderColor: LEVEL_COLORS[level] }}
>
Click
</button>
);
爽点爆炸:
- 样式和逻辑真正 colocated(放在一起),组件自包含;
- 动态样式信手拈来,JS 表达式直接嵌入;
- 主题系统天然支持,通过 React Context 透传;
- DevTools 调试时能看到组件名(如
Button-emotion),比 hash 友好多了。
但代价也不小:
- 运行时需要解析模板字符串、注入
<style>标签,首屏性能有损耗; - SSR 配置复杂,稍不注意就会出现 hydration 不匹配警告;
- 对非 JS 开发者(比如纯 HTML 切图仔)极不友好;
- 最大的雷:如果没做好缓存,每次 re-render 都可能生成新 class,导致不必要的重排重绘。
数据说话:我们测了什么?
为了不被“我觉得”带偏,我们用 Lighthouse 和自定义脚本跑了三组数据(基于同一页面,10 次取平均):
| 指标 | 传统 CSS | CSS Modules | Emotion (CSS-in-JS) |
|---|---|---|---|
| 首屏 FCP (ms) | 820 | 840 | 960 |
| Bundle Size (gzipped) | 28KB | 29KB | 35KB (+7KB 运行时) |
| 构建时间 (s) | 4.2 | 4.5 | 5.8 |
| 样式冲突率 | 3/20 页面 | 0 | 0 |
| 动态样式开发效率 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
注:测试环境为 MacBook Pro M1, Webpack 5, React 18, Node 18
可以看到,CSS-in-JS 在开发体验上碾压,但性能和体积确实有代价。尤其在低端安卓机上,FCP 差距能拉到 200ms 以上——这对电商活动页来说,可能就是转化率差 1% 的问题。
我们的选择:混合策略,务实为王
经过一个月的灰度实验,我们最终没有“全盘切换”,而是采用了分层策略:
- 基础组件库(Button, Card, Input):用 CSS Modules。因为这些组件样式稳定、复用率高,不需要频繁动态调整。
- 业务复杂组件(活动 Banner、个性化推荐流):用 Emotion。这里需要大量根据用户数据、ABTest 配置动态调整样式,CSS-in-JS 的表达力无可替代。
- 全局样式(重置、字体、栅格):依然用传统 CSS,通过 CDN 缓存,确保首屏最快加载。
同时,我们定了三条铁律:
- 禁止在 CSS-in-JS 中写复杂逻辑:比如
props => props.isLoading ? 'spin' : 'none'可以,但别在里面调 API 或算斐波那契数列; - 所有动态样式必须 memoize:用
useMemo或 Emotion 的css缓存能力,避免重复生成; - SSR 必须开启 extractCritical:确保首屏 HTML 包含关键 CSS,避免 FOUC。
// next.config.js 中配置 Emotion SSR
const withEmotion = (config) => {
config.optimization.splitChunks.cacheGroups.styles = {
name: 'styles',
test: /\.(css|sass|scss|less)$/,
chunks: 'all',
enforce: true,
};
return config;
};
工具链与学习资源
说到工具,不得不提几个救命稻草:
- GitHub 上的
emotion-server示例:官方仓库里的 next.js 集成 demo 救了我们 SSR 的命; - VSCode 插件
vscode-styled-components:语法高亮 + 智能提示,写 CSS-in-JS 不再是瞎子摸象; - Chrome DevTools 的 Coverage 面板:用来分析哪些 CSS 实际未被使用,配合 PurgeCSS 做清理。
至于学习,我强烈推荐两本书:
- 《CSS in Depth》:虽然不讲 CSS-in-JS,但把传统 CSS 的坑讲透了,理解底层才能更好用新方案;
- 《React Cookbook》里的样式章节:实战导向,直接告诉你怎么在真实项目中组织样式。
另外,杭州本地的技术沙龙也帮了大忙。上个月在网易参加的一个前端 meetup,有个阿里 P7 分享了他们如何用 Linaria(零运行时的 CSS-in-JS)做高性能活动页,听得我直拍大腿——原来还能这么玩!
写在最后:没有银弹,只有权衡
回到开头那个周五晚上的崩溃时刻。现在我们用混合方案后,类似问题基本绝迹。上周双11压测,首屏 FCP 稳定在 900ms 内,设计小姐姐终于没再追着我问“为什么按钮变胖了”。
作为一线技术负责人,我越来越觉得:技术选型不是追求最潮,而是找到最适合当前团队、当前业务、当前约束的解。CSS-in-JS 很酷,但如果你的团队还在维护 IE11,或者你的页面全是静态内容,那可能纯 CSS + Utility Class(比如 Tailwind)更香。
所以别被社区的 hype 带跑偏。打开 GitHub,看看那些 star 数高的项目是怎么做的;翻翻书籍,理解每种方案的设计哲学;最重要的是——在你的真实项目里跑一跑。
毕竟,代码最终是要上线的,不是发推特的。
(完)
P.S. 如果你也在杭州,欢迎来参加我们每月组织的“西湖边 CSS 啃饼会”——名字是我起的,其实就是找个咖啡馆吹牛。上次聊到凌晨一点,差点被老板扣绩效。

评论 0