CSS-in-JS vs 传统CSS:现代样式方案选择指南
大家好,我是阿哲。一个在北京中关村附近创业公司搬砖的全栈开发——说白了就是那种“前端后端都得会、数据库崩溃了也得修、连产品经理画的原型图歪了都要我调”的打杂型程序员。每天通勤一小时(回龙观→国贸,懂的都懂),晚上回家还得研究点新东西,不然怕被卷死。
上个月我们团队搞双11大促活动页重构,产品那边突然丢来一句:“要丝滑动效,还要主题可换,最好还能按用户分组展示不同颜色……” 我当时就坐在工位上盯着屏幕,心里默默问候了一遍祖宗十八代。但骂归骂,活还得干。于是我就不得不重新思考:到底该用传统的 CSS 文件,还是拥抱 CSS-in-JS?
这篇文章不是什么高深理论,纯粹是我在项目实战中踩坑、翻车、又爬起来后的血泪总结。如果你也在纠结 React 项目里怎么写样式更优雅、更高效、更不容易被测试同学提一堆 UI 偏移 bug,那这篇教程或许能帮你少走点弯路。
一切的开始:那个“简单”的需求
事情是这样的。我们有个营销页面,原本用的是普通的 .scss + BEM 命名规范,结构清晰、编译快、浏览器兼容性稳如老狗。但这次产品提的需求有点离谱:
- 主题色要根据用户来源渠道动态切换(比如微信进来的用红色,抖音进来的用紫色)
- 某些区域要有 hover 动画 + 微交互动效(别小看这个,做不好就卡成 PPT)
- 后期可能还要支持暗黑模式
- 最关键的是:下周三上线!
我第一反应是:“这不就是加几个 CSS 变量的事?”
结果刚改完,测试同学跑来:“阿哲,iOS 12 上动画卡顿,而且有些按钮颜色没变。”
我:“……行吧。”
这时候我意识到:传统 CSS 在动态性和 JS 集成度上的短板,开始暴露了。
传统 CSS 的优点:稳、快、省心
先别急着喷传统 CSS。说实话,在很多场景下,它依然是王者。
我们团队之前所有后台管理系统、数据看板,清一色用 SCSS + PostCSS + CSS Modules。为啥?因为:
- 构建速度快:Webpack 打包时样式文件单独处理,不拖慢 JS bundle。
- 浏览器原生支持:没有运行时开销,首屏渲染快到飞起。
- 调试方便:DevTools 里直接看到类名,改个颜色立马生效。
- 团队协作友好:设计师给 Figma,前端对照写类,互不干扰。
特别是我们这种小公司,人手紧、迭代快,谁有时间天天折腾 fancy 的新方案?能跑就行。
举个真实例子:去年我们搞一个内部运维面板,用纯 CSS Grid + Flex 布局,三天搞定,上线零样式 bug。测试同学甚至夸我:“这次居然没偏移!”
但问题来了——一旦需要和 JavaScript 深度联动,传统 CSS 就显得力不从心。
比如动态主题?你得靠 JS 去 document.documentElement.style.setProperty('--primary-color', color),再配合大量 CSS 变量。逻辑分散在两个文件里,维护起来像在玩“找不同”。
再比如条件样式:user.isVip ? 'button-vip' : 'button-normal' —— 看似简单,但类名越来越多,最后 .button-vip-special-limited-edition 这种名字都快出来了,BEM 直接破防。
CSS-in-JS:把样式塞进 JS 的“邪道”
于是我把目光投向了 CSS-in-JS。主流方案有 Styled Components、Emotion、Linaria 等。我们选了 Emotion,因为它支持 css prop、性能好、还能和 SSR 完美配合(我们用 Next.js)。
为什么没选 Styled Components?因为 bundle size 大了一点点,而我们对首屏加载速度极其敏感(产品说“用户多等 100ms 就跑了”——虽然我觉得他在吹牛)。
用 Emotion 写样式大概是这样的:
import { css } from '@emotion/react'
const Button = ({ variant, children }) => {
return (
<button
css={css`
padding: 8px 16px;
border-radius: 4px;
background: ${variant === 'primary' ? '#3b82f6' : '#e5e7eb'};
color: ${variant === 'primary' ? 'white' : 'black'};
transition: all 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
`}
>
{children}
</button>
)
}
看起来是不是很爽?样式和组件逻辑绑在一起,变量直接从 props 拿,不用再翻半天 SCSS 文件。
更绝的是动态主题:
// theme.js
export const theme = {
colors: {
primary: '#3b82f6',
secondary: '#10b981'
}
}
// Button.jsx
import { useTheme } from '@emotion/react'
const ThemedButton = () => {
const theme = useTheme()
return <button css={{ backgroundColor: theme.colors.primary }} />
}
一行代码搞定主题切换,再也不用手动 setProperty。
而且 Emotion 支持 自动 vendor prefix、critical CSS 提取(SSR 场景下只注入当前页面用到的样式),甚至还能 避免类名冲突——因为它生成的是哈希类名,比如 css-1a2b3c,根本不怕全局污染。
踩坑实录:不是所有光鲜都无代价
但别急着欢呼。CSS-in-JS 也不是银弹。我在双11项目里至少踩了三个大坑。
坑一:首屏性能差点翻车
上线前一天压测,发现 Lighthouse 分数掉了 10 分。排查发现:Emotion 在客户端注入样式是异步的,导致首屏出现 FOUC(Flash of Unstyled Content)——按钮先是默认样式,0.3 秒后才变蓝。
解决方案:必须配合 SSR 把 critical CSS 内联到 <head>。Emotion 官方文档其实写了,但我一开始偷懒没配,结果被运维大哥追着问:“你们前端是不是又搞什么花里胡哨的东西?”
配完 SSR 后:
// _app.js (Next.js)
import { CacheProvider } from '@emotion/react'
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'
const cache = createCache({ key: 'css' })
const { extractCritical } = createEmotionServer(cache)
function MyApp({ Component, pageProps, emotionStyleTags }) {
return (
<CachePRovider value={cache}>
<head>
{emotionStyleTags}
</head>
<Component {...pageProps} />
</CacheProvider>
)
}
MyApp.getInitialProps = async (ctx) => {
const initialProps = await App.getInitialProps(ctx)
const styles = extractCritical(renderPage().html)
return {
...initialProps,
emotionStyleTags: styles.css.map(style => (
<style
key={style.key}
dangerouslySetInnerHTML={{ __html: style.css }}
data-emotion={style.key}
/>
))
}
}
搞定。FOUC 消失,Lighthouse 分数回来了。但说实话,配置成本比纯 CSS 高多了。
坑二:调试体验降级
以前 DevTools 里看到 .btn-primary,现在看到 css-1x2y3z。虽然 Emotion 插件能显示源码位置,但断点调试样式?不存在的。
有一次动画卡顿,我硬是花了两小时才定位到是某个 transform 触发了 layout thrashing。要是用传统 CSS,直接在 Sources 里改值试效果,分分钟搞定。
坑三:bundle size 膨胀
虽然 Emotion 运行时很小(~5KB gzipped),但每个组件都带样式字符串,JS bundle 还是变大了。我们监控发现,首页 JS 体积增加了 12%。
后来我们做了 code splitting + 懒加载非首屏组件,才勉强压回去。但这也意味着:CSS-in-JS 更适合 SPA 或复杂交互应用,不适合内容型、SEO 优先的页面。
对比表格:一目了然
为了帮自己理清思路,我做了个对比表(顺便分享给你们):
| 维度 | 传统 CSS (SCSS + Modules) | CSS-in-JS (Emotion) |
|---|---|---|
| 动态样式支持 | 弱(依赖 CSS 变量或 class 切换) | 强(直接使用 JS 变量) |
| 主题切换 | 需手动管理 CSS 变量 | 内置 ThemeProvider,一行切换 |
| 作用域隔离 | 靠命名规范或 Modules | 自动哈希类名,天然隔离 |
| 构建速度 | 快(样式独立编译) | 稍慢(需处理 JS 中的样式) |
| 运行时性能 | 无开销 | 少量(但可忽略) |
| SSR 支持 | 天然支持 | 需额外配置 critical CSS |
| 调试体验 | 极佳(DevTools 直接编辑) | 一般(需插件,类名不可读) |
| Bundle Size | 样式单独提取,JS 小 | 样式嵌入 JS,体积略增 |
| 学习成本 | 低(所有前端都会) | 中(需理解新 API) |
| 适合场景 | 静态页面、后台系统、内容站 | 动态 UI、复杂交互、设计系统 |
我的最终选择:混合策略
经过这次双11血战,我悟了:没有最好的方案,只有最合适的组合。
我们现在采用 混合策略(Hybrid Approach):
- 基础布局、通用组件(按钮、输入框) → 用传统 SCSS + CSS Modules,保证稳定和性能。
- 高度动态、交互复杂的模块(比如活动页、可视化图表) → 用 Emotion,享受 JS 驱动样式的便利。
- 全局主题变量 → 用 CSS 变量定义,同时在 Emotion 的
theme里同步一份,两边都能用。
比如:
/* globals.css */
:root {
--color-primary: #3b82f6;
}
// theme.js
export const theme = {
colors: {
primary: 'var(--color-primary)' // 兜底,也可直接写死
}
}
这样,即使某个组件还没迁移到 Emotion,也能读到主题色。
给 React 开发者的建议
如果你正在用 React,纠结要不要上 CSS-in-JS,我的建议是:
- 别为了用而用。如果你的项目是博客、电商详情页、CMS,老老实实用传统 CSS,省心省力。
- 如果是复杂交互应用(比如设计器、仪表盘、游戏化界面),CSS-in-JS 能极大提升开发体验。
- 务必配好 SSR,否则 FOUC 会让你在上线前夜失眠。
- 不要全盘迁移。可以先在一个新模块试点,跑通流程再推广。
- 关注 bundle size 和 Lighthouse 分数,别让“炫技”拖垮用户体验。
结语:技术没有银弹,只有权衡
写这篇文章的时候,已经是凌晨一点。窗外北京的夜色很静,只有键盘敲击声。回想上周五晚上加班到十点,终于把主题切换 bug 修好,那一刻真的想请自己喝杯奶茶。
作为创业公司的全栈,我深知:技术选型不是比谁用的工具最新,而是谁能用最稳的方式按时交付。
CSS-in-JS 是一把锋利的刀,能切出精致的 UI,但也可能割伤自己。传统 CSS 是把钝斧,笨重但可靠。关键看你砍的是什么木头。
希望这篇带点自嘲、带点干货的教程,能帮你少熬几个夜,少砸几台电脑。
毕竟,程序员的命也是命,对吧?
—— 阿哲,于北京回龙观出租屋,2024年6月

评论 0