CSS-in-JS vs 传统CSS:现代样式方案选择指南
上周五晚上,我戴着降噪耳机听着 Radiohead 的《No Surprises》,正打算改完最后一个按钮的 hover 样式就下班。结果产品经理突然发来消息:“这个组件在深色模式下文字看不清……”
我叹了口气——这已经是本周第三次了。
在这家公司待了三年多,从 Vue 2 写到 React + TypeScript 全家桶,眼看着团队从“能跑就行”进化到开始讨论 SSR、微前端和设计系统。可说到样式管理,我们好像还在原地打转:全局 class 名冲突、主题切换写得像缝合怪、动态样式硬是用 style={{}} 拼出来……线上出过几次样式 bug,运维大哥半夜打电话说我写的 CSS 把首页布局搞崩了(其实锅在第三方库,但背锅侠总是我)。
最近在考虑跳槽,面试时被问了好几次“你们怎么处理样式?用 CSS Modules 还是 Emotion?” 我支支吾吾说“都用过”,结果对方眼神里透着“你是不是只会写 <style> 标签”。
行吧,逼自己一把。趁着周末没约会(程序员常态),我决定把 CSS-in-JS 和传统 CSS 方案彻底捋一遍。这篇不是教程,而是踩坑实录+选型思考——毕竟我这种“手写代码老派”现在也开始用 GitHub Copilot 辅助了(虽然经常要重写它生成的💩代码)。
起因:一个让人崩溃的“简单需求”
事情得从去年双11说起。我们做了一个促销弹窗组件,要求:
- 支持浅色/深色主题
- 动态根据用户等级显示不同颜色(VIP 是金色,普通用户是蓝色)
- 在移动端需要隐藏某些元素
- 需要 SSR 兼容(SEO 很重要)
最开始我用的是 SCSS + BEM 命名规范:
.promo-modal {
&__header {
color: $text-primary;
}
&--vip {
.promo-modal__header { color: gold; }
}
}
但问题很快就来了:
- 主题切换要用 JS 动态替换
<html>的 class,还得配合 CSS 变量,逻辑散落在各处 - 用户等级样式得靠父组件传 className,组件耦合严重
- 移动端媒体查询写得又臭又长
- 最致命的是:SSR 时样式闪动(FOUC),因为 CSS 文件加载有延迟
上线后第二天,测试妹子提了个 Bug:“VIP 用户在深色模式下,金色文字看不清”。我翻遍 SCSS 文件,发现 $gold 在深色背景下的对比度不够。改?可以。但得同时改 SCSS 变量、媒体查询、主题映射表……当时真的想砸电脑。
那一刻我意识到:传统的 CSS 架构,在复杂交互和动态需求面前,已经力不从心了。
方案一:拥抱 CSS-in-JS(但别太热情)
我先是试了 Emotion —— 目前社区最活跃的 CSS-in-JS 库之一,GitHub 上 star 数快 15k 了,Next.js 官方也推荐。
装包:
npm install @emotion/react @emotion/styled
然后改造组件:
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
const getTextColor = (userLevel, theme) => {
if (theme === 'dark') {
return userLevel === 'vip' ? '#FFD700' : '#4A90E2'
}
return userLevel === 'vip' ? '#D4AF37' : '#007BFF'
}
const PromoModal = ({ userLevel, theme }) => (
<div
css={css`
color: ${getTextColor(userLevel, theme)};
@media (max-width: 768px) {
display: none;
}
`}
>
限时优惠!
</div>
)
优点真香
- 动态样式直接写 JS 逻辑:不用再拼 className 或 toggle class
- 自动作用域隔离:再也不用担心
.button污染全局 - 主题切换丝滑:配合
ThemeProvider,一行代码换肤 - SSR 支持完善:Emotion 会自动把关键样式内联到
<style>标签,解决 FOUC
但坑也不少
- 性能开销:每次渲染都要执行函数生成样式。虽然 Emotion 有缓存,但在列表组件里如果没 memo,还是会卡。
- 调试困难:浏览器 DevTools 里看到的是一堆
css-xxxxx,虽然能点进去看源码,但不如传统 class 直观。 - 资源体积增加:引入 runtime,gzip 后大概 +5~8KB。对首屏性能敏感的项目要掂量。
- TypeScript 类型支持一般:虽然能用,但 autocomplete 不如 SCSS 变量那么稳。
小插曲:第一次部署到测试环境,发现所有样式都没了。查了半天,原来是忘记在
_app.tsx里包裹CacheProvider。Emotion 的 SSR 配置文档藏得有点深……
方案二:传统 CSS 的现代化演进
说实话,作为老派程序员,我还是对纯 CSS 有执念。于是研究了 CSS Modules + CSS Variables + PostCSS 的组合。
CSS Modules:告别命名战争
/* PromoModal.module.css */
.header {
color: var(--text-color);
}
.vipHeader {
color: var(--vip-color);
}
@media (max-width: 768px) {
.mobileHidden {
display: none;
}
}
import styles from './PromoModal.module.css'
const PromoModal = ({ userLevel, theme }) => {
const headerClass = userLevel === 'vip'
? styles.vipHeader
: styles.header
return (
<div className={`${headerClass} ${styles.mobileHidden}`}>
限时优惠!
</div>
)
}
配合 PostCSS 的 postcss-custom-properties 插件,可以在 JS 里动态设置 CSS 变量:
// themeManager.js
export const setTheme = (themeName) => {
document.documentElement.style.setProperty(
'--text-color',
themeName === 'dark' ? '#E0E0E0' : '#333333'
)
document.documentElement.style.setProperty(
'--vip-color',
themeName === 'dark' ? '#FFD700' : '#D4AF37'
)
}
优势很明显
- 零运行时:没有额外 JS,首屏快
- DevTools 友好:class 名带 hash(如
header_d3f4k),一眼看出来源文件 - 兼容性好:IE11 以上都能跑(只要不用 CSS 变量部分)
- 资源优化成熟:Webpack 可以提取公共 CSS,HTTP/2 下并行加载
但动态能力弱
- 主题切换依赖 DOM 操作,SSR 时得在服务端注入
<style>片段 - 复杂逻辑(比如“如果用户是 VIP 且在移动端且是 iOS”)写起来很啰嗦
- CSS 变量不支持 fallback 以外的逻辑判断
硬核对比:一张表说清楚
| 维度 | CSS-in-JS (Emotion) | 传统 CSS (Modules + Variables) |
|---|---|---|
| 作用域隔离 | ✅ 自动 | ✅ 通过 hash |
| 动态样式 | ✅ 直接写 JS | ⚠️ 需 CSS 变量 + DOM 操作 |
| SSR 支持 | ✅ 内置优化 | ⚠️ 需手动注入关键 CSS |
| 首屏性能 | ⚠️ +5~8KB JS | ✅ 纯 CSS,无 JS 开销 |
| 调试体验 | ⚠️ class 名抽象 | ✅ 源码映射清晰 |
| 主题切换 | ✅ Context 驱动 | ⚠️ 需管理 CSS 变量状态 |
| TypeScript 支持 | ⚠️ 一般 | ✅ 可配合 d.ts 声明文件 |
| 学习成本 | ⚠️ 需理解新概念 | ✅ 熟悉 CSS 即可 |
| GitHub 生态 | ✅ 丰富(styled-components, Emotion) | ✅ 成熟(Sass, Less, PostCSS) |
注:数据基于个人项目实测 + Webpack Bundle Analyzer 分析
安全意识:别让样式成为 XSS 入口
这里必须提一嘴安全。CSS-in-JS 如果处理不当,可能引发 XSS!
比如这段危险代码:
// 千万别这么写!
<div css={{ backgroundColor: userInputColor }} />
如果 userInputColor 是 'red;}</style><script>alert(1)</script><style>{',就可能注入脚本。
Emotion 默认会对值做 escape,但如果你用 css 标签的字符串形式:
css`
background: ${userInput}; // 危险!
`
它不会转义。正确做法是:
- 对用户输入做校验(只允许 hex/rgb)
- 或使用 Emotion 的
unsafeCSS显式标记(但慎用) - 或坚持用对象语法(自动 escape)
传统 CSS 也有风险,比如 style={{ backgroundColor: userInput }},但至少不会污染全局样式。
记住:任何来自用户的输入,都不能直接拼接到样式中。 我们团队去年就因为一个富文本编辑器的颜色 picker 漏洞,差点被渗透测试打穿。
我的选择:混合策略 + 资源治理
折腾一个月后,我向团队提出了 分层策略:
基础组件(Button, Card) → 用 CSS Modules
理由:静态、复用率高、性能敏感业务组件(PromoModal, Dashboard) → 用 Emotion
理由:动态逻辑多、主题依赖强全局样式/重置 → 保留 传统 CSS
理由:reset.css 这种不需要作用域
同时做了几件事提升 资源管理:
- 在
package.json里加了browserslist,确保 PostCSS 和 Autoprefixer 覆盖目标设备 - 用 Webpack 的
splitChunks把 CSS 提取为单独文件,避免 JS 阻塞 - 在 GitHub Actions 里加了 CSS 体积监控,超过阈值就报警
- 所有 CSS-in-JS 组件必须写单元测试,覆盖主题和响应式场景
效果?双11大促期间,页面 FCP(First Contentful Paint)提升了 0.3s,运维大哥终于没半夜 call 我了。
写在最后:工具是手段,不是目的
回看这段历程,其实没有“银弹”。CSS-in-JS 不是洪水猛兽,传统 CSS 也没过时。关键在于 匹配团队能力和项目需求。
如果你团队全是 React 老手,项目重度依赖动态主题,那 Emotion/Styled Components 能极大提升开发体验。
但如果你在做内容型网站(比如博客、新闻站),追求极致性能,那 SCSS + PurgeCSS 可能更合适。
至于我?虽然现在也会用 GitHub 上的开源方案,但写核心逻辑时还是习惯关掉 Copilot,手敲每一行代码——那种掌控感,AI 给不了。
下周就要去新公司面试了。如果被问到样式方案,我会说:“看场景。不过我已经准备好两套方案的 demo 了,要看看吗?”
(完)
附:避坑清单
- ❌ 别在 CSS-in-JS 里写复杂计算(比如
width: calc(100% - ${sidebarWidth}px)),容易触发重排 - ✅ 用
@emotion/babel-plugin自动标签 class 名,方便调试 - ❌ 别滥用
!important,无论是哪种方案 - ✅ 传统 CSS 项目务必开启
cssnano压缩 - 🔒 用户输入的颜色值,一定要用
tinycolor2校验合法性
希望这篇带血泪的经验,能帮你少熬几个夜。毕竟,程序员的时间,应该花在听 Radiohead 上,而不是 debug 样式冲突 😅

评论 0