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

Rust练习生
2025-12-13 13:14
阅读 665

上周五晚上十点半,我坐在公司最后一盏还亮着的灯下,盯着一个线上样式的诡异错位——按钮在 Safari 上莫名其妙地多了一层阴影,在 Chrome 却完美无瑕。产品同学已经在群里@了三次:“老板说这个双11主会场必须零视觉 bug。”运维兄弟幽幽来了一句:“你确定不是又用了什么 fancy 的新工具?”

那一刻我真的想砸键盘。

我是老李,杭州一家三线互联网公司的技术负责人。虽然说是“负责人”,但其实团队就七个人,前端俩、后端仨、测试一个、产品经理……算了他不算人。我们公司离网易园区走路十五分钟,阿里西溪园区打车二十块起步。说实话,在这种地方做技术,既没大厂资源,又得顶住业务压力,常常是“既要又要还要”——既要快速迭代,又要稳定上线,还要兼容 IE(别笑,真的还有客户用!)。

最近半年,我们在重构一个核心运营活动平台,正好碰上了样式架构选型的问题。传统 CSS 还是 CSS-in-JS?这个问题看似简单,实则牵一发动全身。今天就想和大家掏心窝子聊聊我们的踩坑经历、权衡思路,以及最终怎么在“理想”和“现实”之间找到那条缝。


起因:一次“优雅”的样式崩坏

事情要从去年双11说起。当时我们用的是最传统的 CSS + BEM 命名规范,配合 Webpack 的 MiniCssExtractPlugin 打包。一切看起来岁月静好,直到某天前端小王为了“提升开发体验”,偷偷在某个新模块里引入了 styled-components

结果呢?两个组件同时用了 .button--primary,一个来自传统 CSS,一个来自 styled-components 动态生成的 class(比如 sc-1a2b3c4-0 kLmNoP),权重打架,样式覆盖混乱。更糟的是,因为 styled-components 把样式注入到 <head><style> 标签里,而传统 CSS 是外链的,加载顺序不可控,导致某些页面在首屏渲染时出现“样式闪烁”(FOUC)。

线上报错不多,但 UI 审查时被设计小姐姐追着问:“为什么同一个按钮,有的圆角有的方角?”我只能苦笑:“因为我们有两个宇宙。”

这件事之后,团队开了个复盘会。有人提议全面拥抱 CSS-in-JS,有人说干脆回归纯 CSS + CSS Modules。吵到最后,我说:“别争了,咱们做个实验——拿真实业务场景测一测。”


真刀真枪:我们做了什么对比?

我们选了三个维度来评估:开发体验、构建性能、运行时表现。每个方案都用在了一个真实的活动页模块上(就是那种三天出稿、五天上线、七天改十版的那种“敏捷”项目)。

方案一:传统 CSS + PostCSS + BEM

这是我们原来的主力方案。优点大家都懂:成熟、稳定、工具链丰富。PostCSS 插件生态强大,Autoprefixer 自动补全前缀,cssnano 压缩体积。BEM 虽然写起来啰嗦,但至少不会命名冲突。

/* activity-card.css */
.activity-card {
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.activity-card__title {
  font-size: 18px;
  color: #333;
}

痛点也很明显

  • 全局作用域,哪怕用了 BEM,也不敢保证没人手滑写了个 .title
  • 无法动态传参(比如根据主题色切换),要么写一堆 modifier class,要么靠 JS 操作 classList;
  • Tree-shaking 几乎无效,哪怕你只用了一个组件,整个 CSS 文件都会被打包进去。

方案二:CSS Modules

这是传统 CSS 的“安全升级版”。通过 Webpack 的 css-loader 启用 modules,自动 hash class 名,彻底解决命名冲突。

// Button.jsx
import styles from './Button.module.css';
export default () => <button className={styles.primary}>Click</button>;

生成的 HTML 会是:

<button class="Button_primary__aB3cD">Click</button>

优点

  • 零冲突,作用域隔离;
  • 支持组合(composes),可以复用基础样式;
  • 构建时处理,运行时无额外开销。

缺点

  • 动态样式依然难搞。比如“根据用户等级显示不同边框颜色”,你得预先定义好 .level-1, .level-2... 然后在 JS 里拼字符串;
  • 调试时 DevTools 里的 class 名是 hash,虽然可以用 [name]__[local] 配置可读,但终究不如直接看类名直观;
  • 和设计师协作时,他们给的 Sketch/Figma 样式名对不上,沟通成本高。

方案三:CSS-in-JS(以 Emotion 为例)

我们试了 styled-components@emotion/react,最终选了 Emotion,因为它支持 SSR 更友好,且有 css prop 这种灵活用法。

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

const buttonStyle = (theme) => css`
  border-radius: 4px;
  background: ${theme.primaryColor};
  &:hover {
    opacity: 0.9;
  }
`;

export default ({ level }) => (
  <button
    css={buttonStyle}
    style={{ borderColor: LEVEL_COLORS[level] }}
  >
    Click
  </button>
);

爽点爆炸

  • 样式和逻辑真正 colocated(放在一起),组件自包含;
  • 动态样式信手拈来,JS 表达式直接嵌入;
  • 主题系统天然支持,通过 React Context 透传;
  • DevTools 调试时能看到组件名(如 Button-emotion),比 hash 友好多了。

但代价也不小

  • 运行时需要解析模板字符串、注入 <style> 标签,首屏性能有损耗;
  • SSR 配置复杂,稍不注意就会出现 hydration 不匹配警告;
  • 对非 JS 开发者(比如纯 HTML 切图仔)极不友好;
  • 最大的雷:如果没做好缓存,每次 re-render 都可能生成新 class,导致不必要的重排重绘。

数据说话:我们测了什么?

为了不被“我觉得”带偏,我们用 Lighthouse 和自定义脚本跑了三组数据(基于同一页面,10 次取平均):

指标 传统 CSS CSS Modules Emotion (CSS-in-JS)
首屏 FCP (ms) 820 840 960
Bundle Size (gzipped) 28KB 29KB 35KB (+7KB 运行时)
构建时间 (s) 4.2 4.5 5.8
样式冲突率 3/20 页面 0 0
动态样式开发效率 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐

注:测试环境为 MacBook Pro M1, Webpack 5, React 18, Node 18

可以看到,CSS-in-JS 在开发体验上碾压,但性能和体积确实有代价。尤其在低端安卓机上,FCP 差距能拉到 200ms 以上——这对电商活动页来说,可能就是转化率差 1% 的问题。


我们的选择:混合策略,务实为王

经过一个月的灰度实验,我们最终没有“全盘切换”,而是采用了分层策略

  • 基础组件库(Button, Card, Input):用 CSS Modules。因为这些组件样式稳定、复用率高,不需要频繁动态调整。
  • 业务复杂组件(活动 Banner、个性化推荐流):用 Emotion。这里需要大量根据用户数据、ABTest 配置动态调整样式,CSS-in-JS 的表达力无可替代。
  • 全局样式(重置、字体、栅格):依然用传统 CSS,通过 CDN 缓存,确保首屏最快加载。

同时,我们定了三条铁律:

  1. 禁止在 CSS-in-JS 中写复杂逻辑:比如 props => props.isLoading ? 'spin' : 'none' 可以,但别在里面调 API 或算斐波那契数列;
  2. 所有动态样式必须 memoize:用 useMemo 或 Emotion 的 css 缓存能力,避免重复生成;
  3. SSR 必须开启 extractCritical:确保首屏 HTML 包含关键 CSS,避免 FOUC。
// next.config.js 中配置 Emotion SSR
const withEmotion = (config) => {
  config.optimization.splitChunks.cacheGroups.styles = {
    name: 'styles',
    test: /\.(css|sass|scss|less)$/,
    chunks: 'all',
    enforce: true,
  };
  return config;
};

工具链与学习资源

说到工具,不得不提几个救命稻草:

  • GitHub 上的 emotion-server 示例:官方仓库里的 next.js 集成 demo 救了我们 SSR 的命;
  • VSCode 插件 vscode-styled-components:语法高亮 + 智能提示,写 CSS-in-JS 不再是瞎子摸象;
  • Chrome DevTools 的 Coverage 面板:用来分析哪些 CSS 实际未被使用,配合 PurgeCSS 做清理。

至于学习,我强烈推荐两本书:

  • CSS in Depth》:虽然不讲 CSS-in-JS,但把传统 CSS 的坑讲透了,理解底层才能更好用新方案;
  • React Cookbook》里的样式章节:实战导向,直接告诉你怎么在真实项目中组织样式。

另外,杭州本地的技术沙龙也帮了大忙。上个月在网易参加的一个前端 meetup,有个阿里 P7 分享了他们如何用 Linaria(零运行时的 CSS-in-JS)做高性能活动页,听得我直拍大腿——原来还能这么玩!


写在最后:没有银弹,只有权衡

回到开头那个周五晚上的崩溃时刻。现在我们用混合方案后,类似问题基本绝迹。上周双11压测,首屏 FCP 稳定在 900ms 内,设计小姐姐终于没再追着我问“为什么按钮变胖了”。

作为一线技术负责人,我越来越觉得:技术选型不是追求最潮,而是找到最适合当前团队、当前业务、当前约束的解。CSS-in-JS 很酷,但如果你的团队还在维护 IE11,或者你的页面全是静态内容,那可能纯 CSS + Utility Class(比如 Tailwind)更香。

所以别被社区的 hype 带跑偏。打开 GitHub,看看那些 star 数高的项目是怎么做的;翻翻书籍,理解每种方案的设计哲学;最重要的是——在你的真实项目里跑一跑

毕竟,代码最终是要上线的,不是发推特的。

(完)

P.S. 如果你也在杭州,欢迎来参加我们每月组织的“西湖边 CSS 啃饼会”——名字是我起的,其实就是找个咖啡馆吹牛。上次聊到凌晨一点,差点被老板扣绩效。

评论 0

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