CSS-in-JS vs 传统CSS:现代样式方案选择指南
去年双11大促前一周,我正在通州出租屋里对着屏幕疯狂敲代码。那天是周五晚上11点,窗外连地铁都停了,而我还在为一个“动态主题切换”的需求抓狂——产品经理说用户要能随时在暗色/亮色/粉色(?)之间无缝切换,还得支持自定义品牌色。当时项目用的是传统的 CSS + SCSS 架构,光是改个变量就得重新构建,更别提运行时动态换肤了。
作为一个在北京外包圈混了4年的老油条,我见过太多奇葩需求:从“按钮要有呼吸感”到“整个网站要像Windows 98”,但这次真的把我整不会了。凌晨2点,我一边啃着冷掉的黄焖鸡,一边在 GitHub 上狂搜解决方案,突然看到了 Emotion 和 Styled Components 的文档……那一刻,我仿佛看到了曙光。
为啥我要折腾这个?
先简单介绍下自己:坐标北京,每天挤1小时地铁去国贸某外包公司搬砖,主业是帮各种甲方爸爸写前端。过去四年,我维护过从 jQuery 到 React 18 的各种项目,踩过的坑比写的代码还多。平时喜欢深夜 coding(白天总被产品经理拉会),也爱折腾新技术,但工作中还是以稳为主——毕竟上线炸了,锅是我背,不是 AI 背。
上周我们接了个新活:给一家做 SaaS 的后端公司重构管理后台。他们技术栈很新(Next.js + TypeScript + Tailwind),但样式系统混乱得像一锅粥:全局 CSS、模块化 CSS、内联 style 混用,连 SVG 图标颜色都要靠 !important 覆盖。更离谱的是,他们的设计师给了三套 UI Kit,每套对应不同客户品牌……
于是领导拍板:“这次必须统一样式方案,你来调研下,下周给个方案。”
我:???又到了熟悉的“背锅侠”时刻。
别急着选型,先看场景
很多教程一上来就吹“CSS-in-JS 是未来”,或者“原生 CSS 才是正道”,但现实哪有这么非黑即白?我在 GitHub 上翻了几十个 star 过万的开源项目,发现大家的选择其实高度依赖业务场景:
- 内部工具 / 后台系统 → 常用 Tailwind 或传统 CSS(快、稳、团队熟悉)
- 面向 C 端的复杂应用 → 倾向 CSS Modules 或 CSS-in-JS(隔离性好、支持动态)
- 组件库开发 → 几乎清一色 CSS-in-JS(便于主题定制、无副作用)
我们这个项目属于第二种:多租户 SaaS,需要强隔离 + 动态主题 + 国际化支持。传统 CSS 的全局污染问题会让我们后期维护成本爆炸。
我试了三个方案,结果出乎意料
方案1:纯传统 CSS + CSS Modules
先试了最保守的方案:用 CSS Modules 实现局部作用域。代码大概长这样:
/* Button.module.css */
.primary {
background: var(--primary-color);
border: none;
padding: 8px 16px;
border-radius: 4px;
}
.primary:hover {
opacity: 0.9;
}
// Button.tsx
import styles from './Button.module.css';
const Button = ({ variant = 'primary' }) => (
<button className={styles[variant]}>Click me</button>
);
优点:
- 团队上手快,运维部署无感知
- 构建速度飞快(Vite 下几乎秒开)
- 浏览器兼容性完美(IE11 都能跑,虽然没人用了)
缺点:
- 动态主题?得靠 CSS 变量硬扛,但 IE 不支持
- 伪类、媒体查询写起来啰嗦
- 组件状态耦合(比如
isActive要手动拼 className)
上周五测试同学提了个 Bug:暗色模式下 hover 效果没生效。我查了半天,发现是因为某个父组件覆盖了 :hover 样式……典型的 CSS 全局污染。
方案2:Tailwind CSS
作为近年最火的原子化 CSS 框架,Tailwind 确实香。我们试了下:
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Click me
</button>
优点:
- 开发效率高,不用切文件
- 响应式、hover、focus 状态一行搞定
- 社区生态强大(GitHub 上相关项目超多)
缺点:
- HTML 变得臃肿(尤其复杂组件)
- 动态主题支持弱(得配合
dark:前缀或 JS 控制 class) - 自定义设计系统成本高(要改
tailwind.config.js)
最致命的是:甲方设计师要求“品牌色精度到小数点后两位”,而 Tailwind 默认色板是离散的。虽然可以自定义,但配置起来比写 CSS 还累。
方案3:CSS-in-JS(Emotion)
最后祭出大杀器:Emotion。这是目前社区活跃度最高、TypeScript 支持最好的 CSS-in-JS 库之一(Styled Components 也不错,但打包体积略大)。
import styled from '@emotion/styled';
import { css } from '@emotion/react';
const Button = styled.button<{ variant: 'primary' | 'secondary' }>`
border: none;
padding: 8px 16px;
border-radius: 4px;
background: ${({ theme, variant }) =>
variant === 'primary' ? theme.colors.primary : theme.colors.secondary};
&:hover {
opacity: 0.9;
}
`;
配合 ThemeProvider:
// theme.ts
export const lightTheme = {
colors: {
primary: '#3b82f6',
secondary: '#64748b'
}
};
export const darkTheme = {
colors: {
primary: '#60a5fa',
secondary: '#94a3b8'
}
};
优点:
- 完美支持动态主题(运行时切换 theme 对象即可)
- 样式与组件强绑定,零污染
- 支持 props 条件渲染(不用拼 className)
- 自动 vendor prefix + 压缩
缺点:
- 初学者可能觉得“JS 里写 CSS 很怪”
- SSR 需要额外配置(不过 Next.js 已内置支持)
- 极端情况有轻微性能损耗(但实际体验无感)
关键对比:一张表说清楚
| 维度 | 传统 CSS / CSS Modules | Tailwind CSS | CSS-in-JS (Emotion) |
|---|---|---|---|
| 学习成本 | ⭐☆☆☆☆ (极低) | ⭐⭐☆☆☆ (低) | ⭐⭐⭐☆☆ (中) |
| 动态主题 | ❌ (需 CSS 变量) | ⚠️ (有限支持) | ✅ (原生支持) |
| 样式隔离 | ✅ (Modules) | ✅ (原子类) | ✅ (自动哈希) |
| 构建性能 | ✅✅✅ | ⚠️ (需 Purge) | ⚠️ (运行时注入) |
| 浏览器兼容 | ✅✅✅ | ✅✅ | ✅ (IE 需 polyfill) |
| 调试体验 | DevTools 直接看 | 类名冗长难读 | 生成 readable class |
| GitHub 生态 | 成熟稳定 | 爆炸增长 | React 社区主流 |
注:打星标准 —— ⭐越多越好,✅ 表示支持良好
我们最终怎么做的?
经过两周的 POC(Proof of Concept),团队投票决定:用 Emotion + 主题系统。原因很简单——甲方爸爸的需求就是“动态换肤”,其他都是次要的。
关键代码分享:
// ThemeProvider 封装
import { ThemeProvider as EmotionThemeProvider } from '@emotion/react';
import { useMemo } from 'react';
const ThemeProvider = ({ children, brandConfig }: Props) => {
const theme = useMemo(() => ({
colors: {
primary: brandConfig.primaryColor || '#3b82f6',
// ...其他动态色值
},
spacing: { sm: 8, md: 16, lg: 24 }
}), [brandConfig]);
return <EmotionThemeProvider theme={theme}>{children}</EmotionThemeProvider>;
};
线上效果:用户在设置页选个颜色,整个界面实时刷新,连图表、图标、文字颜色都同步变了。测试同学惊呼:“这需求原来真能实现?”
踩过的坑 & 最佳实践
1. 别滥用 inline style
CSS-in-JS 不等于把所有样式塞进 style={{}}。Emotion 的 styled 或 css prop 才是正道,它会生成 <style> 标签而非内联属性,避免重复渲染。
2. SSR 务必配置
如果你用 Next.js,记得在 _app.tsx 引入 CacheProvider,否则服务端渲染的样式会丢失:
import { CacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';
// 详细配置参考 Emotion 官方 Next.js 示例
3. 性能监控
虽然 Emotion 有缓存机制,但在列表渲染大量组件时,建议用 shouldForwardProp 过滤无效 props:
const Box = styled('div', {
shouldForwardProp: (prop) => !['isLoading'].includes(prop)
})`
opacity: ${({ isLoading }) => isLoading ? 0.5 : 1};
`;
4. 调试技巧
安装 Emotion DevTools 插件,可以在 Chrome DevTools 里直接看到组件对应的样式规则,还能实时编辑——比找 class 名快多了。
给后端同事的友情提示
我知道很多后端兄弟看到前端花样翻新就头疼。但这次选型,其实对你们也有好处:
- 减少环境差异:样式逻辑收归组件,不再依赖全局 CSS 文件加载顺序
- 简化部署:所有样式内联或注入 head,不怕 CDN 缓存旧 CSS
- API 更清晰:主题配置通过 JSON 透传,不用再传一堆 class name
上周运维大哥还夸我:“终于不用半夜 call 我处理 CSS 加载 404 了。”
写在最后
回到开头那个双11的夜晚——如果当时我知道 Emotion 的 @emotion/css 可以这样写动态样式:
import { css } from '@emotion/css';
const getThemeClass = (color) => css`
--primary: ${color};
background: var(--primary);
`;
可能就不会熬到凌晨三点了 😭
但话说回来,没有银弹。如果你在做一个内部数据看板,明天就要上线,那老老实实用 CSS Modules 最稳妥;但如果你在打造一个需要长期迭代、多品牌适配的平台级产品,CSS-in-JS 的收益远大于学习成本。
现在我的 GitHub 仓库里已经建了个模板项目:saas-starter-kit,整合了 Emotion + Theme + i18n,欢迎 star(求求了,就差 3 个 star 破百了)。
外包狗的日子不好过,但每次搞定一个看似不可能的需求,那种“老子天下第一”的爽感,真的会上瘾。共勉。
P.S. 产品经理刚又提了个新需求:“能不能让按钮点击时有粒子爆炸效果?”
我:……要不您试试 Framer Motion?

评论 0