CSS-in-JS vs 传统CSS:现代样式方案选择指南
上周五晚上十点,办公室只剩我和隔壁工位的测试小哥。他还在复现我下午提测的一个样式 bug——“在 Safari 上按钮错位了半像素”。我盯着屏幕右下角闪烁的 npm run dev 日志,左手握着已经凉透的瑞幸,右手在键盘上狂敲 console.log('why?')。那一刻真的想砸电脑。
但转念一想,这不就是我们这些前端狗的日常吗?尤其是在杭州这片“大厂林立、卷成麻花”的土地上。我在一家中型互联网公司干了三年多,团队主要用 React + TypeScript 搞业务系统,去年开始疯狂上 AI 相关功能(老板说要“拥抱未来”),我也被迫从只会写 <div className="card"> 的人,变成了每天和 Claude、ChatGPT 讨论“如何优雅地做动态主题切换”的“伪专家”。
最近,因为想跳槽(阿里网易的机会确实多,但面试官总爱问“你们项目用什么样式方案?”),我不得不重新审视一个老生常谈的问题:到底该用 CSS-in-JS 还是传统 CSS?
这篇文章,就当是我给自己的技术复盘,也顺便帮那些和我一样在 deadline 前夜纠结样式的兄弟姐妹们理清思路。
起因:一个“简单”的需求,引发一场样式战争
事情要从上个月说起。产品同学拿着 PRD 走过来,笑眯眯地说:“我们要做一个可配置的主题系统,支持深色/浅色模式,还要允许用户自定义主色调。” 我当时心里咯噔一下——这不就是传说中的“动态主题”嘛?
我们的项目是典型的 React SPA,之前一直用 传统的 CSS Modules,配合一些 BEM 命名规范,勉强维持体面。但这次需求一出,问题立马暴露:
- 如何在运行时切换颜色变量?
- 如何避免全局污染(毕竟有些第三方组件用了
!important)? - 如何保证 SSR(服务端渲染)下主题一致?
我第一反应是:“加个 CSS 变量不就完了?” 确实,CSS Variables (:root { --primary: #007aff; }) 在现代浏览器支持得不错。但很快被现实打脸——我们有个老旧的客户还在用 IE11(别问,问就是政企项目),而且部分嵌入式设备只支持到 Chrome 68。
于是,我开始翻文档、看 GitHub Issues、在 Stack Overflow 搜“dynamic theme react”,结果掉进了一个叫 CSS-in-JS 的兔子洞。
CSS-in-JS 是啥?真有那么神?
先说结论:CSS-in-JS 不是一种具体工具,而是一类将 CSS 写在 JavaScript 里的范式。代表选手有 styled-components、Emotion、Linaria 等。
它的核心思想很简单:把样式当作组件状态的一部分。比如用 Emotion(现在社区更推荐它,因为性能更好、支持 SSR 更稳):
// Button.jsx
import { css } from '@emotion/react';
const primaryButton = (theme) => css`
background-color: ${theme.primary};
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
`;
export const Button = ({ children, theme }) => (
<button css={primaryButton(theme)}>{children}</button>
);
看起来很酷对吧?主题色直接从 theme 对象里取,JavaScript 控制一切。而且因为是动态生成的 <style> 标签,天然支持运行时切换。
但问题也来了:
- 性能开销:每次组件渲染都可能生成新样式(虽然 Emotion 有缓存机制)
- 调试困难:DevTools 里看到的 class 名是
css-1x2y3z,根本不知道对应哪个组件 - 学习成本:新人看了直呼“这是 JS 还是 CSS?”
我当时在本地搞了个 PoC(Proof of Concept),跑起来发现首屏加载慢了 200ms。运维同事看到 Lighthouse 报告后直接在群里 at 我:“兄弟,FCP 又飘了,是不是又在玩新花样?”
传统 CSS 真的过时了吗?
别急着抛弃传统 CSS。其实,现代 CSS 已经进化得相当能打了。
方案一:CSS Modules + CSS Variables(渐进增强)
这是我们目前主力方案。结构清晰,编译时隔离,还能利用 PostCSS 做自动前缀、压缩等优化。
/* Button.module.css */
.button {
background-color: var(--primary, #007aff);
color: white;
/* ... */
}
// Button.jsx
import styles from './Button.module.css';
export const Button = ({ children }) => (
<button className={styles.button}>{children}</button>
);
优点:
- 构建时生成唯一 class 名,避免冲突
- 支持 tree-shaking(未使用的样式会被 PurgeCSS 干掉)
- DevTools 里 class 名可读(比如
Button_button__1a2b3)
缺点:
- 动态主题需要配合 JS 动态修改
:root变量 - 对于复杂交互(比如 hover 时改变兄弟元素样式),还是得靠 JS 操作 class
方案二:Tailwind CSS(原子化 CSS)
最近在准备跳槽面试时,发现阿里 P7 面试官特别爱问 Tailwind。于是我硬着头皮学了一周。
Tailwind 的思路是:用 utility classes 组合样式,而不是写自定义 CSS。
<button className="bg-blue-500 text-white px-4 py-2 rounded">
Click me
</button>
配合 @apply 还能抽象出组件级 class:
/* components.css */
.btn-primary {
@apply bg-blue-500 text-white px-4 py-2 rounded;
}
优点:
- 开发速度飞快,不用切文件
- 天然响应式(
md:bg-red-500) - 支持 JIT 模式,按需生成 CSS,体积极小
缺点:
- HTML 变得臃肿(class 属性可能超长)
- 主题定制需要改
tailwind.config.js - 对设计师不友好(他们习惯给
.button而不是bg-blue-500)
实战对比:三个方案在真实项目中的表现
为了验证效果,我用同一个“主题切换”需求,在内部 demo 项目里分别实现了三种方案(React 18 + Vite + TypeScript)。以下是关键指标对比:
| 方案 | 首屏 FCP (ms) | Bundle Size (CSS) | 动态主题支持 | 调试体验 | 学习曲线 |
|---|---|---|---|---|---|
| CSS Modules + Variables | 850 | 24 KB | ✅(需 JS 配合) | ⭐⭐⭐⭐ | ⭐⭐ |
| Emotion (CSS-in-JS) | 1050 | 31 KB (+ JS 运行时) | ✅(原生支持) | ⭐⭐ | ⭐⭐⭐ |
| Tailwind CSS | 780 | 18 KB (JIT) | ✅(通过 class 切换) | ⭐⭐⭐ | ⭐⭐⭐⭐ |
测试环境:MacBook Pro M1, Chrome 124, 模拟 Fast 3G 网络
可以看到:
- Tailwind 性能最好,但需要团队接受它的哲学
- Emotion 功能最强大,但 bundle size 和 FCP 有代价
- CSS Modules 最平衡,适合渐进式改造
我最后选了谁?以及为什么
经过两周的折腾、三次线上灰度、还有一次因为主题切换导致按钮消失的 P0 事故(别提了,是忘了加 !important),我们最终采用了 CSS Modules + CSS Variables + 少量 Emotion 补丁 的混合方案。
具体策略如下:
- 基础组件(Button、Input 等):用 CSS Modules + CSS Variables 定义,保证性能和可维护性
- 动态强交互组件(比如根据数据实时变色的图表):用 Emotion 写,利用其动态能力
- 全局主题切换逻辑:用 React Context + JS 动态设置
document.documentElement.style.setProperty('--primary', newColor)
关键代码片段:
// ThemeProvider.jsx
import { createContext, useContext, useEffect } from 'react';
const ThemeContext = createContext();
export const useTheme = () => useContext(ThemeContext);
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
useEffect(() => {
// 同步到 CSS Variables
document.documentElement.style.setProperty('--primary', theme === 'dark' ? '#1e40af' : '#3b82f6');
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
/* global.css */
:root {
--primary: #3b82f6; /* 默认浅色 */
}
[data-theme='dark'] {
--primary: #1e40af;
}
这样既保留了传统 CSS 的性能优势,又能在必要时用 CSS-in-JS 解决棘手问题。
给正在纠结的你:几点血泪建议
别为了用新技术而用新技术
我见过太多团队因为“styled-components 很潮”就全盘切换,结果 SSR 渲染出一堆空 div,SEO 直接崩掉。记住:技术是为业务服务的。考虑团队现状
如果你团队里还有人在写 jQuery,突然上 Emotion,等于制造技术债。渐进式改造才是王道。性能永远是底线
尤其是 B 端或政企项目,用户可能用的是十年前的笔记本。Lighthouse 分数低于 80?赶紧优化!跳槽前先搞懂原理
面试官问“CSS-in-JS 和 CSS Modules 区别”,不是让你背定义,而是看你是否理解:作用域隔离、运行时 vs 编译时、SSR 兼容性这些底层逻辑。Go 语言工程师都在笑
(开玩笑)但说实话,前端样式方案的碎片化程度,连后端 Go 开发者都看不懂。不过这也正是前端的魅力所在——永远在折腾,永远在进步。
结语:在混沌中寻找秩序
写这篇文章的时候,已经是凌晨一点。窗外杭州的雨下个不停,我的终端还在跑 vite build --mode production。明天还要和产品经理对焦“主题色要不要支持渐变”这种玄学需求。
但回过头看,从最初只会复制粘贴 Bootstrap,到现在能权衡不同样式方案的利弊,我觉得自己总算有点“高级前端”的样子了(虽然工资还没涨)。
技术没有银弹,CSS-in-JS 不是救世主,传统 CSS 也没死。关键是在具体场景下,选择那个让你少加班、少背锅、多摸鱼的方案。
如果你也在杭州,也在考虑跳槽,也在为样式方案头疼——欢迎留言交流。说不定哪天我们在阿里的食堂、网易的咖啡厅,还能一起吐槽:“当年那个 Safari 半像素 bug,真是害我掉了好多头发。”
共勉。

评论 0