CSS-in-JS vs 传统CSS:现代样式方案选择指南

独立开发路上
2025-12-15 23:14
阅读 503

上周五晚上十点,办公室只剩我和隔壁工位的测试小哥。他还在复现我下午提测的一个样式 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 补丁 的混合方案。

具体策略如下:

  1. 基础组件(Button、Input 等):用 CSS Modules + CSS Variables 定义,保证性能和可维护性
  2. 动态强交互组件(比如根据数据实时变色的图表):用 Emotion 写,利用其动态能力
  3. 全局主题切换逻辑:用 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 解决棘手问题。


给正在纠结的你:几点血泪建议

  1. 别为了用新技术而用新技术
    我见过太多团队因为“styled-components 很潮”就全盘切换,结果 SSR 渲染出一堆空 div,SEO 直接崩掉。记住:技术是为业务服务的

  2. 考虑团队现状
    如果你团队里还有人在写 jQuery,突然上 Emotion,等于制造技术债。渐进式改造才是王道。

  3. 性能永远是底线
    尤其是 B 端或政企项目,用户可能用的是十年前的笔记本。Lighthouse 分数低于 80?赶紧优化!

  4. 跳槽前先搞懂原理
    面试官问“CSS-in-JS 和 CSS Modules 区别”,不是让你背定义,而是看你是否理解:作用域隔离、运行时 vs 编译时、SSR 兼容性这些底层逻辑。

  5. Go 语言工程师都在笑
    (开玩笑)但说实话,前端样式方案的碎片化程度,连后端 Go 开发者都看不懂。不过这也正是前端的魅力所在——永远在折腾,永远在进步。


结语:在混沌中寻找秩序

写这篇文章的时候,已经是凌晨一点。窗外杭州的雨下个不停,我的终端还在跑 vite build --mode production。明天还要和产品经理对焦“主题色要不要支持渐变”这种玄学需求。

但回过头看,从最初只会复制粘贴 Bootstrap,到现在能权衡不同样式方案的利弊,我觉得自己总算有点“高级前端”的样子了(虽然工资还没涨)。

技术没有银弹,CSS-in-JS 不是救世主,传统 CSS 也没死。关键是在具体场景下,选择那个让你少加班、少背锅、多摸鱼的方案

如果你也在杭州,也在考虑跳槽,也在为样式方案头疼——欢迎留言交流。说不定哪天我们在阿里的食堂、网易的咖啡厅,还能一起吐槽:“当年那个 Safari 半像素 bug,真是害我掉了好多头发。”

共勉。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝