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

黄勇·
2025-12-19 11:43
阅读 1427

上周五晚上10点半,我还在阿里园区的工位上和一个诡异的样式冲突死磕。页面在本地跑得好好的,一上预发环境就炸——按钮文字错位、颜色乱跳,连产品经理都跑来问:“你们前端是不是又改了 UI?” 我心里一万个草泥马奔腾而过,但嘴上还得笑着说“正在排查”。

说起来有点好笑,作为一个从 DBA 转型过来的后端开发(没错,就是那个天天和 EXPLAIN、索引优化、慢查询日志打交道的老家伙),我现在居然在写 React 组件、纠结 CSS 方案。这事得从三年前说起:那会儿公司搞云原生转型,K8s + 微服务 + 前端工程化三件套齐上,领导一句“后端也得懂点前端”,我就被“逼”着学 React。结果一发不可收拾,现在每天早上8点准时到工位,第一件事不是看数据库监控,而是 npm run dev

今天这篇文章,就是想和大家聊聊我在做新项目时,在 CSS-in-JS传统 CSS 之间反复横跳的心路历程。不灌鸡汤,不吹概念,全是实战踩坑后的血泪总结。


起因:一个“简单”的需求,引发样式战争

事情是这样的。我们团队要重构一个内部运维控制台(对,就是那种只有 SRE 和 DBA 才会用的“祖传系统”)。UI 框架定了 Ant Design Pro,技术栈是 React + TypeScript。产品经理轻描淡写地说:“这次要支持主题切换,深色/浅色模式一键切换,最好还能让不同 BU 自定义品牌色。”

我一听就头皮发麻。传统 CSS 怎么搞?写一堆 .theme-dark .btn { ... }?还是靠 CSS 变量?可我们的老系统里还有 jQuery 写的模块,全局样式污染严重,改一处可能崩三处。更别说有些组件是从旧项目 copy 过来的,class 名叫 red-btn-important-do-not-touch —— 真的,这不是段子。

这时候,组里新来的应届生小张(前端科班出身)提议:“哥,试试 CSS-in-JS 吧!styled-components 或 Emotion,作用域隔离天然解决冲突,动态主题也超简单。”

我内心是抗拒的。毕竟我骨子里还是个 DBA:一切以性能、确定性、可维护性为先。CSS 文件不是好好的吗?为啥要把样式塞进 JS 里?这不是“为了炫技而炫技”?

但 deadline 不等人。双11压测就在两周后,这系统要是崩了,运维兄弟们怕是要把我挂树上。于是,我决定亲自下场,做个对比实验。


实战对比:两种方案怎么写?

场景:一个带 hover 效果、支持主题色的按钮

方案一:传统 CSS + CSS Modules(我们目前的“折中”方案)

/* Button.module.css */
.btn {
  padding: 8px 16px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
  transition: background-color 0.2s;
}

.btn.primary {
  background-color: var(--primary-color);
  color: white;
}

.btn.primary:hover {
  background-color: var(--primary-hover-color);
}
// Button.tsx
import styles from './Button.module.css';

const Button = ({ type = 'default', children }) => {
  return (
    <button className={`${styles.btn} ${styles[type]}`}>
      {children}
    </button>
  );
};

优点很明显:结构清晰,CSS 该干啥干啥,JS 只负责逻辑。而且打包后 CSS 是单独文件,缓存友好。

但问题也来了:

  • --primary-color 得靠全局注入,万一某个页面忘了加载主题变量?
  • 如果要根据 props 动态计算样式(比如 size="large" 改变 padding),就得写更多 class 或内联 style,优雅度下降。
  • 多人协作时,有人手抖写了 .btn { width: 100% !important; },整个系统就裂开了。

方案二:Emotion(CSS-in-JS 的佼佼者)

// Button.tsx
import { css } from '@emotion/react';

const getButtonStyles = (theme, type) => css`
  padding: 8px 16px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
  transition: background-color 0.2s;
  
  ${type === 'primary' && `
    background-color: ${theme.primaryColor};
    color: white;
    
    &:hover {
      background-color: ${theme.primaryHoverColor};
    }
  `}
`;

const Button = ({ type = 'default', children }) => {
  return (
    <button css={getButtonStyles(useTheme(), type)}>
      {children}
    </button>
  );
};

看着代码是不是有点“反直觉”?样式居然藏在函数里,还用了模板字符串!但神奇的是:

  • 样式完全组件化,不存在全局污染。
  • theme 是 React Context 传下来的,切换主题只需更新 context,所有组件自动响应。
  • 动态逻辑直接写在 JS 里,不用额外维护 class 映射。

我当时第一次看到这种写法,内心 OS:这不就是把 CSS 当字符串拼接吗?性能能好吗?


性能 & 兼容性:别被“理论”忽悠了

作为 DBA 出身的人,我对“性能”两个字异常敏感。于是我做了个小测试:

方案 首屏 FCP (ms) Bundle Size (gzip) 主题切换延迟
CSS Modules 320 12KB ~200ms(需重载 CSS)
Emotion (SSR) 340 15KB <50ms(纯 JS 更新)

数据来自 Lighthouse + Webpack Bundle Analyzer,环境是 Next.js 13 + React 18。

结论很意外:CSS-in-JS 的运行时开销其实没那么可怕,尤其在 SSR 场景下,Emotion 会把关键 CSS 内联到 HTML 中,首屏体验几乎无损。而传统方案的主题切换,往往需要动态加载新的 CSS 文件或操作 <style> 标签,反而有闪烁。

不过要注意:如果你还在支持 IE11(对,有些国企项目真的要),CSS-in-JS 的自动 vendor prefix 可能不够全,得手动配 PostCSS。而传统 CSS + Autoprefixer 就稳如老狗。


团队协作与可维护性:这才是大头

技术选型从来不只是技术问题。

在我们团队(杭州某大厂,你懂的),前后端分离明显,但后端经常要改点 UI。以前他们改 CSS,动不动就 break 掉别人的布局。现在用 Emotion,每个组件样式独立,后端同学只改自己文件里的 css 字符串,根本不会影响别人——甚至有人说:“这比写 SQL 还直观!”

当然,也有翻车现场。有一次实习生把 css 函数写成了普通字符串,结果样式没生效,页面白茫茫一片。我 debug 半小时才发现他漏了 @emotion/react 的 import。这种“魔法”对新人确实有门槛。

另外,传统 CSS 在 DevTools 里调试方便,class 名清晰;而 CSS-in-JS 生成的 class 名是哈希(比如 css-1a2b3c),虽然 Emotion 提供了 label 选项,但还是不如直接看 .header-title 来得爽。


我的选择:看场景,别站队

经过这个项目,我的结论是:

  • 如果你在做高度动态、主题驱动、组件库级别的应用(比如设计系统、SaaS 后台),CSS-in-JS(尤其是 Emotion)值得投入。它的组件化思想和 React 完美契合。
  • 如果你在做内容型网站、营销页,或者对 SEO/首屏性能极度敏感,传统 CSS + CSS Modules + 构建时优化(PurgeCSS 等)仍是王者。
  • 混合方案也可行:核心组件用 CSS-in-JS,全局 reset / layout 用传统 CSS。我们现在的项目就这么干。

最后吐槽一句:别听某些“前端布道师”说“CSS 已死”。CSS 是 web 的基石,只是工具在进化。就像我们 DBA 也不会因为有了 ORM 就不用懂 SQL 一样。


给想尝试 CSS-in-JS 的 React 新手的小教程

  1. 安装 Emotion:

    npm install @emotion/react @emotion/styled
    
  2. _app.tsx(Next.js)或根组件包裹 CacheProvider(可选,用于 SSR 优化)

  3. 写组件:

    import { css } from '@emotion/react';
    
    const Title = () => (
      <h1 css={css`
        color: ${props => props.theme.primary};
        font-size: 2rem;
      `}>
        Hello
      </h1>
    );
    
  4. 配合 ThemeProvider:

    import { ThemeProvider } from '@emotion/react';
    
    const theme = { primary: '#1890ff' };
    
    <ThemeProvider theme={theme}>
      <App />
    </ThemeProvider>
    

搞定。是不是比想象中简单?


写完这篇,已经是凌晨1点。窗外杭州下着小雨,工位上咖啡凉了。突然想起当年调优 MySQL 参数的日子——其实做技术哪有什么银弹,只有不断权衡、试错、再权衡。

希望这篇文章能帮你少走点弯路。如果你们也在纠结样式方案,不妨拉个分支,写个 demo 对比一下。毕竟,代码不会骗人,但 PPT 会

(完)

评论 0

最热最新
暂无评论
黄勇·Lv.1
0
影响力
0
文章
0
粉丝