CSS-in-JS vs 传统CSS:现代样式方案选择指南
上周五晚上十点半,办公室只剩我和隔壁工位的运维小哥还在“坚守阵地”。他忙着处理凌晨大促的服务器扩容,我则对着一个诡异的样式冲突抓耳挠腮——某个按钮在 Chrome 正常,但在 Safari 上莫名其妙地多了一层阴影。产品经理明天一早就要 demo,而我连问题出在哪都没搞清楚。
那一刻,我真的想砸电脑。
不过说来也巧,就在我疯狂 console.log + !important 的时候,耳机里刚好播到 Radiohead 的《No Surprises》,歌词里那句 “a heart that’s full up like a landfill” 突然让我顿悟:我的 CSS 文件,不就是个填满了过期样式的垃圾场吗?
作为一个在同一家公司待了三年多、至今仍坚持手写每一行代码的老派前端(是的,我连 VS Code 的自动补全都关了,纯粹是强迫症),我最近却开始被迫接触一些“新玩意儿”——比如 AI 辅助编程,还有今天要聊的主角:CSS-in-JS。
为什么?因为领导在 Q2 技术规划会上拍板:“咱们得拥抱现代化,别老用十年前那一套。” 言下之意:你这老古董也该更新一下技术栈了。再加上我自己也在偷偷刷 LeetCode、看 Hugging Face 文档,准备年底跳槽换个环境——毕竟,谁不想在简历上写点“React + Emotion + TypeScript”的高大上组合呢?
所以,今天这篇不是什么权威教程,就是一个保守派前端在真实项目里踩坑、试错、甚至被线上 bug 打脸后的一点开发心得。如果你也正纠结于该用传统的 .css 文件还是时髦的 CSS-in-JS,希望我的经历能帮你少走点弯路。
一切的起点:那个“全局污染”的噩梦
先说说我们公司的技术栈吧。主站是 React + Webpack + Less,样式基本靠 BEM 命名规范撑着。听起来挺标准?但现实很骨感。
去年双11前,我们接了个紧急需求:给商品详情页加个“限时秒杀”弹窗。时间紧任务重,我随手建了个 FlashSaleModal.less,写了几十行样式。结果上线第二天,测试小姐姐一脸严肃地找我:“首页的轮播图怎么变窄了?”
我一脸懵,查了半天才发现——我在 .modal-container 里用了 width: 80%,而首页轮播图的类名恰好叫 .container。虽然按理说作用域不同,但由于 Webpack 的 chunk 拆分策略问题,这两个文件被打包进了同一个 CSS bundle,导致样式互相污染。
那一刻我才意识到:传统 CSS 的“全局性”在大型项目里就是一颗定时炸弹。
当然,你可以用 CSS Modules 或者 Scoped CSS 来解决。我们也试过,但团队里新人太多,有人忘了加 :global,有人把类名拼错,还有人直接在组件里写内联 style……久而久之,代码库变得像一锅乱炖。
于是,我开始认真研究 CSS-in-JS。
初探 CSS-in-JS:真香还是智商税?
第一次听说 CSS-in-JS 是在 GitHub 上逛某个开源项目时。看到人家用 styled-components 写组件,样式和逻辑紧耦合,还能动态传 props 改样式,我当时内心 OS:“这不就是把 CSS 塞进 JS 里搞事情吗?性能能好吗?”
但好奇心害死猫。我本地新建了个小项目,装了 @emotion/react(为啥选 Emotion?因为 Next.js 官方推荐,而且据说比 styled-components 轻量),写了个简单的按钮:
// Button.jsx
import { css } from '@emotion/react';
const primaryButton = (theme) => css`
background-color: ${theme.colors.primary};
border: none;
padding: 12px 24px;
border-radius: 8px;
color: white;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease;
&:hover {
background-color: ${theme.colors.primaryDark};
}
`;
export default function Button({ children, variant = 'primary' }) {
return (
<button css={variant === 'primary' ? primaryAssistant : secondaryStyle}>
{children}
</button>
);
}
跑起来一看——哇,样式完全隔离!就算我在十个组件里都叫 button,也不会互相干扰。更爽的是,我可以直接用 JS 变量控制颜色、间距,甚至根据 props 动态切换主题。
再也不用担心全局污染了!
但很快,问题来了。
性能焦虑:运行时生成样式真的好吗?
我拿 Chrome DevTools 的 Performance 面板测了一下,发现每次组件渲染,Emotion 都会动态生成 <style> 标签插入 <head>。虽然它有缓存机制(相同的样式只生成一次),但在 SSR 场景下,首屏 HTML 里会塞满内联样式,体积暴涨。
有一次我给一个列表页加了 50 个带 hover 效果的卡片,每个卡片都有独立的 CSS-in-JS 样式。结果 Lighthouse 分数从 90+ 掉到 60,FCP(First Contentful Paint)慢了近 1 秒。运维小哥直接在群里 at 我:“兄弟,你这页面是不是在挖矿?”
后来查文档才知道,Emotion 提供了 @emotion/css 和 extractCritical 这种 SSR 优化方案,但配置起来比传统 CSS 复杂多了。对于习惯了 import './style.css' 的我来说,简直像从自行车换到了战斗机——功能强,但操作手册厚。
传统 CSS 的“回光返照”:Tailwind 是救星吗?
就在我对 CSS-in-JS 产生动摇时,隔壁组开始推 Tailwind CSS。
说实话,一开始我是拒绝的。看着满屏的 className="flex justify-between items-center p-4 bg-gray-100 rounded-lg",我觉得这简直是语义化的倒退。但架不住人家构建快、体积小、还能 Purge 无用样式。
我试着在一个新项目里用 Tailwind + 传统 CSS 混搭:布局用 Tailwind,定制化样式(比如动画、复杂交互)写在单独的 .module.css 里。
效果出乎意料地好。
- 构建速度飞快(Vite + Tailwind 几乎秒开)
- 样式体积压缩后只有 8KB
- 团队新人上手极快,毕竟 class 名都是语义化的英文单词
但问题也明显:当 UI 需求高度定制时,Tailwind 的 utility-first 反而成了束缚。比如产品经理突然说:“这个按钮的 hover 效果要加个波纹扩散动画”,你只能回到传统 CSS 写 keyframes,或者硬凑十几个 animate-xxx 类——那画面太美我不敢看。
综合对比:没有银弹,只有权衡
经过几个月的折腾,我整理了一份粗略的对比表(基于我们团队的真实项目数据):
| 维度 | 传统 CSS / CSS Modules | CSS-in-JS (Emotion) | Tailwind CSS |
|---|---|---|---|
| 作用域隔离 | 需手动管理(BEM/Modules) | 自动隔离,天然组件化 | 依赖 class 命名,需规范 |
| 动态样式 | 需用 CSS 变量或 JS 操作 DOM | 直接传 props,非常灵活 | 需配合 style 属性或自定义插件 |
| SSR 支持 | 原生支持,简单直接 | 需额外配置(如 extractCritical) |
原生支持,PurgeCSS 自动优化 |
| 构建性能 | 快(尤其 Vite) | 中等(运行时注入) | 极快(按需生成) |
| Bundle 体积 | 小(可 Tree Shaking) | 中(运行时库 ~10KB) | 极小(Purge 后 <10KB) |
| 学习成本 | 低(前端必修课) | 中(需理解 JS 闭包、缓存) | 低(但需适应 utility 思维) |
| 调试体验 | DevTools 直接编辑 | 样式在 <style> 标签里,难定位 |
class 太多,HTML 很臃肿 |
注:数据基于 React 18 + Vite 3 + Emotion 11 的实测环境
从这张表能看出什么?没有哪一种方案是完美的。关键看你项目的规模、团队的技术栈、以及对“开发体验”和“运行时性能”的优先级排序。
我的最终选择:混合策略 + 渐进式改造
现在,我在新项目里采用了一种“混合策略”:
- 基础布局 & 常用组件:用 Tailwind 快速搭建,保证一致性
- 复杂交互 & 动画:用 Emotion 写 CSS-in-JS,利用其动态能力
- 遗留模块:继续用 CSS Modules,逐步重构,不搞一刀切
比如一个商品卡片组件:
// ProductCard.jsx
import { css } from '@emotion/react';
import clsx from 'clsx';
const cardHoverEffect = css`
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(0,0,0,0.15);
}
`;
export default function ProductCard({ product, isPromoted }) {
return (
<div
className={clsx(
"p-4 bg-white rounded-xl shadow-sm", // Tailwind 基础样式
{ "ring-2 ring-yellow-400": isPromoted } // 条件样式
)}
css={cardHoverEffect} // 复杂交互用 Emotion
>
<h3 className="font-bold text-lg">{product.name}</h3>
{/* ... */}
</div>
);
}
这样既享受了 Tailwind 的开发效率,又保留了 CSS-in-JS 的灵活性。而且,因为 Emotion 只用在少数需要动态样式的组件上,整体 bundle 体积和性能影响几乎可以忽略。
一点真诚的建议:别为技术而技术
写到这里,我想起刚入行时的一个教训。那时为了炫技,非要在项目里用 SASS 的 mixin 和 function 搞一套“设计系统”,结果维护成本高到爆炸,最后被 leader 骂:“你写的代码,除了你自己没人看得懂。”
现在回头看,技术选型的核心不是“新不新”,而是“适不适合”。
- 如果你是一个 3 人小团队,快速迭代 MVP,Tailwind + 传统 CSS 足够;
- 如果你在做高度定制的 Design System 或主题切换应用,CSS-in-JS 的动态能力会省下大量 hack;
- 如果你在维护一个 5 年以上的老项目,别急着全盘替换,用 CSS Modules 逐步隔离更稳妥。
顺便提一句,我在 GitHub 上看了很多开源项目的样式方案,发现连 React 官方文档站都用的是传统 CSS + CSS Modules,而像 Vercel、Shopify 这类现代平台则大量使用 CSS-in-JS。这说明什么?大厂也在根据场景做选择,而不是盲目跟风。
结语:保守派的“妥协”与成长
作为一个习惯边听 Radiohead 写代码、至今还手敲每行 CSS 的“老顽固”,我承认自己曾经对 CSS-in-JS 有偏见。但现实教育了我:前端的世界没有永恒的最佳实践,只有不断演进的解决方案。
现在,我会在合适的场景用 Emotion,也会毫不犹豫地写 .module.css,甚至偶尔用内联 style(别 judge 我,紧急修复线上 bug 时真香)。AI 辅助工具我也开始试了——虽然大部分时候只是用来生成重复的 boilerplate,但至少能让我多喝两口咖啡。
如果你也在考虑跳槽、学新技术,我的建议是:先搞懂原理,再决定用不用。GitHub 上有无数 demo 和最佳实践,多 clone 下来看看,别光听别人吹。
最后,分享一句我贴在显示器边的话:“Don’t rewrite, refactor.” —— 别想着推倒重来,学会在现有基础上渐进优化,才是工程师的成熟之道。
好了,音乐播到下一首了,是 Arcade Fire 的《Ready to Start》。我得去改那个 Safari 的阴影 bug 了——这次,我决定用 CSS 变量 + @supports 来兼容,而不是 !important。
祝大家都能写出干净、高效、不让人半夜惊醒的样式代码。

评论 0