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

MQ堵车了
2025-12-14 16:24
阅读 669

上个月,我在 GitHub 上给一个开源组件库提了个 PR,结果被 maintainer 狠狠教育了一顿:“兄弟,你这 class 命名太随意了,跟 JavaScript 变量混在一起,维护性堪忧啊。” 我一脸懵,心想我这命名明明清晰又语义化,咋就成“随意”了?后来一聊才知道,人家团队全面拥抱 CSS-in-JS,而我还在用 SCSS + BEM 的老派写法。

这事让我反思了好几天。作为一个从 DBA 转型过来的后端开发(对,就是那个整天盯着慢查询、死锁、索引优化的人),我对代码结构和可维护性的执念,其实比很多前端还重。数据库讲究范式、一致性、事务隔离——这些理念迁移到前端工程里,不也该体现在样式的组织方式上吗?

最近在准备跳槽,一边刷 LeetCode,一边疯狂补前端新知识。上周五晚上加班到十点,产品经理突然在钉钉群里甩来一句:“这个按钮 hover 效果要加个微动效,明天上线。” 我差点把咖啡泼到键盘上——我们项目用的是纯 CSS Modules,改个动画得先建个新 class,再 import,还得担心全局污染……那一刻,我真想试试 Emotion 或者 Styled Components。

于是,我决定系统梳理一下:在现代前端工程中,我们到底该怎么选样式方案?


一场关于“样式归属”的哲学争论

说白了,CSS-in-JS 和传统 CSS 的核心分歧,不是语法问题,而是**关注点分离(Separation of Concerns)**的理解差异。

传统派认为:“HTML 负责结构,CSS 负责样式,JS 负责行为”——这是教科书级的分层思想。而 CSS-in-JS 派则反驳:“组件化时代,样式本就是组件逻辑的一部分,强行拆出去反而割裂了封装。”

这话听着耳熟吧?就像当年我们在数据库设计时争论“该不该用 JSON 字段存复杂结构”——关系型信徒说“破坏范式”,NoSQL 爱好者说“灵活才是王道”。

但现实哪有非黑即白?我在去年双11大促前重构支付页面时就深有体会:一个“立即支付”按钮,在不同营销活动中要动态换色、换圆角、甚至换字体粗细。如果用传统 CSS,要么写一堆 button--campaign-abutton--campaign-b,要么在 JS 里动态拼 class 名——后者简直像在 SQL 里拼字符串,随时可能注入漏洞(虽然只是样式混乱)。

而用 Styled Components,直接传 props 就行:

import styled from 'styled-components';

const PayButton = styled.button`
  background: ${props => props.themeColor || '#007aff'};
  border-radius: ${props => props.rounded ? '24px' : '4px'};
  font-weight: ${props => props.bold ? '700' : '400'};
  transition: all 0.2s ease;
  
  &:hover {
    transform: scale(1.03);
  }
`;

// 使用
<PayButton themeColor="#ff6b6b" rounded bold>立即支付</PayButton>

看到没?样式逻辑和组件状态耦合得天衣无缝。这不就是我们 DBA 常说的“数据与业务紧耦合”吗?只不过这里,“数据”是 UI 状态,“业务”是视觉表现。


真实战场:我在项目中的踩坑记录

我们团队目前维护着一个中后台管理系统,技术栈是 React + Ant Design + Less。早期为了快速上线,样式管理相当混乱:全局变量乱用、class 命名随意、媒体查询散落在各处。结果某次迭代,测试同学报了个 Bug:“用户列表页的筛选框在 Safari 上错位了。”

我花了两小时排查,最后发现是因为某个同事在 global.less 里加了:

input {
  box-sizing: border-box;
}

但忘了 Safari 对某些 input 类型的默认样式处理不同……这种“隐式副作用”简直是我这种 DBA 出身的人的噩梦——就像有人在生产库偷偷改了默认隔离级别却不告诉你。

于是我们决定引入 CSS Modules 作为过渡方案。效果立竿见影:每个组件的样式自动 scoped,再也不怕全局污染。但问题也随之而来:

  • 动态主题切换?得靠 CSS Variables + JS 动态设置 :root,或者用 classnames 手动拼接。
  • 条件样式?写一堆 ternary operator 在 className 里,可读性爆炸。
  • 性能?每个组件生成独立 CSS 文件,HTTP 请求增多(虽然后续可以合并,但构建配置变复杂)。

这时候,隔壁组用 Emotion 的同事看不下去了,甩给我一段代码:

import { css } from '@emotion/react';

const getButtonStyle = (props) => css`
  padding: ${props.size === 'large' ? '12px 24px' : '8px 16px'};
  color: ${props.disabled ? '#ccc' : props.textColor};
  cursor: ${props.disabled ? 'not-allowed' : 'pointer'};
`;

function MyButton({ size, disabled, textColor, children }) {
  return (
    <button
      css={getButtonStyle({ size, disabled, textColor })}
    >
      {children}
    </button>
  );
}

我试了下,真香!条件逻辑直接写在样式里,不用绕弯子。而且 Emotion 支持 SSR、自动 vendor prefix、关键 CSS 提取——这些在传统 CSS 里得靠 PostCSS 插件链堆出来。

但转头我就被运维大哥警告了:“你们前端包体积又涨了 15%!CDN 流量费用要超预算了!” ——原来 CSS-in-JS 的运行时(runtime)会增加 bundle size。虽然可以通过 @emotion/babel-plugin 做编译时优化,但配置起来比写个 .postcssrc 麻烦多了。


性能、兼容性、调试体验:不能只看表面

我知道很多老派开发者(包括我自己刚开始)会说:“CSS-in-JS 运行时开销大,影响首屏性能。” 但真相是:现代方案已经极大优化了这个问题

以 Emotion 为例,开启 sourceMapautoLabel 后,你在 DevTools 里看到的 class 名是这样的:

css-1wq8r9a-MyButton

而不是以前那种 aB3cD 的随机字符串。更绝的是,它还能和 React DevTools 联动,直接在组件树里看到样式规则!

至于性能,我在本地做了个简单 benchmark(用 Lighthouse):

方案 FCP (ms) Bundle Size (KB) TTI (ms)
Pure CSS + Modules 820 120 950
Emotion (runtime) 860 145 980
Emotion (compiled) 830 128 960

差距几乎可以忽略。而且 compiled 模式下,Emotion 会把样式提取成静态 CSS,和传统方案无异。

不过,IE11 用户慎入。虽然有些 CSS-in-JS 库声称支持 IE,但实际用起来各种 polyfill 塞满 webpack config,还不如直接用 Sass + Autoprefixer 省心。


一张对比表,帮你快速决策

结合我最近刷题、面经、GitHub issues 里的讨论,整理了这张实战对比表:

维度 传统 CSS (SCSS/Less + Modules) CSS-in-JS (Emotion/Styled Components)
学习成本 低(前端基本功) 中(需理解 JS 作用域、闭包)
动态样式 靠 CSS Variables / 多 class 切换 直接传 props,天然支持
主题切换 需配合 JS 动态修改 :root 通过 ThemeProvider 全局注入
Tree Shaking 难(除非用 PurgeCSS) 自动(未使用的样式不会打包)
调试体验 DevTools 直接编辑 CSS 需 source map,但可读性已改善
SSR 支持 原生支持 需额外配置(如 extractCritical
Bundle Size 小(纯静态资源) 中(运行时 ~5-10KB,可编译时消除)
TypeScript 支持 弱(需 .d.ts 声明) 强(样式即函数,类型推导自然)
团队协作 需严格规范 BEM/命名 组件即文档,自包含性强

注:数据基于 React 18 + Webpack 5 环境,实测于 2024 年 6 月

如果你团队里有设计师用 Figma,并且要求“像素级还原+动态主题”,那 CSS-in-JS 几乎是必选项。但如果你在维护一个政府项目,用户还在用 IE8(别笑,真有),那就老老实实用原生 CSS 吧。


我的选择:混合架构 + 渐进式演进

经过几轮折腾,我们最终采用了 混合策略

  • 基础组件(Button, Input, Layout)用 Emotion 编写,利用其动态能力和类型安全;
  • 页面级样式仍用 CSS Modules,避免过度抽象;
  • 全局 reset 和 utility classes(比如 .text-center, .hidden)保留为传统 CSS,通过 CDN 单独加载。

构建配置如下(webpack.config.js 片段):

module.exports = {
  module: {
    rules: [
      // Emotion 编译时优化
      {
        test: /\.(js|jsx|ts|tsx)$/,
        use: [
          {
            loader: '@emotion/babel-plugin',
            options: {
              sourceMap: true,
              autoLabel: 'dev-only',
              labelFormat: '[local]--[filename]',
            },
          },
        ],
      },
      // CSS Modules 用于页面样式
      {
        test: /\.module\.less$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: { modules: true },
          },
          'less-loader',
        ],
      },
    ],
  },
};

上线两周,零样式相关线上事故。产品经理再也没半夜钉钉我改按钮样式了(感动哭)。


写在最后:代码人生,没有银弹

回想起刚转前端那会儿,我还嘲笑过 CSS-in-JS 是“为了炫技”。现在才明白,工具的选择,本质是对工程复杂度的认知

就像我们在数据库选型时,不会因为 PostgreSQL 功能强大就抛弃 MySQL——要看业务场景、团队能力、维护成本。

我在 GitHub 上 star 过上百个前端项目,发现一个有趣现象:高 Star 数的 UI 库(如 Material-UI、Chakra UI)几乎全部采用 CSS-in-JS;而工具类库(如 date-fns、axios)则完全不用。这说明什么?当“样式”成为核心产品的一部分时,封装性和动态性就变得至关重要。

所以,别再问“哪个更好”了。问问自己:

  • 我的组件需要频繁根据状态改变样式吗?
  • 团队能否接受额外的学习曲线?
  • 项目的生命周期有多长?是否值得投入架构升级?

上周面试一家独角兽公司,面试官问我:“你怎么看待 CSS-in-JS?” 我没直接回答,反问:“你们的设计系统是动态主题吗?SSR 场景多吗?” 他愣了一下,笑着说:“看来你是真干过活的。”

代码人生,从来不是非此即彼。而是在无数 deadline、线上 bug、产品经理的“小需求”中,找到那个刚刚好的平衡点。

共勉。

评论 0

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