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

协程在摸鱼
2025-12-18 05:53
阅读 1875

昨天凌晨3点,宝宝终于睡了。我悄悄摸出笔记本,打开VS Code,准备刷一道LeetCode——结果发现GitHub上新提了个PR,评论写着:“这个按钮在Dark Mode下颜色没对齐UI稿,上线前必须fix!”

我盯着屏幕,揉了揉酸胀的眼睛,心里默默翻了个白眼:又是样式问题。这已经是本周第4次因为“主题色不一致”被QA打回来的bug了。

作为一名边带娃边写代码的全职妈妈,白天要应付产品的需求变更、测试的连环追问、运维的“你这包太大了CDN扛不住”,晚上还要哄睡+刷面试题挑战(最近在偷偷准备跳槽,别告诉我老板)。时间紧得像挤牙膏,哪还有精力反复调试CSS优先级?

但现实是,样式架构选型真的不能糊弄。它直接影响开发效率、维护成本,甚至线上体验。去年双11大促前,我们团队就因为用错了样式方案,导致紧急回滚——那场面,堪比深夜喂奶时手抖打翻温奶器。

今天这篇,就结合我这几年踩过的坑、熬过的夜、吵过的会,聊聊 CSS-in-JS 和传统CSS到底怎么选。不是理论派,纯实战视角,带娃间隙写的,可能有点碎,但绝对真实。


起因:一个“简单”的主题切换需求

事情得从上周说起。产品经理突然跑来:“我们要做深色模式!下个版本上线!”
我说:“OK,多久?”
他说:“两周后。”
我:“……行吧。”

问题来了:我们项目用的是纯传统CSS + BEM命名规范。虽然结构清晰,但全局变量少,主题色全靠硬编码。比如:

/* button.css */
.btn-primary {
  background-color: #2563eb; /* 蓝色 */
  color: white;
}

现在要支持深色模式,要么写两套.btn-primary--dark,要么用CSS变量。前者冗余爆炸,后者……我们的最低兼容是iOS 9,Safari 9还不支持var()

正当我纠结要不要重构整个样式体系时,隔壁组的小王(刚从某大厂跳槽过来)悠悠地说:“你们怎么不用CSS-in-JS?动态主题切换一行代码搞定。”

我心头一震——这玩意儿我早有耳闻,但一直觉得“过度设计”,毕竟以前做运营后台,样式稳如老狗,哪需要这么花哨?

可这次不一样。用户端产品,交互复杂,主题多变,还涉及A/B测试和运营活动页——传统CSS的静态性开始暴露短板。


我试了三种方案,踩了三个坑

坑1:硬上CSS变量,结果Safari崩了

先试最轻量的方案:CSS自定义属性(Custom Properties)。

:root {
  --primary-color: #2563eb;
}

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

然后JavaScript动态切换:

document.documentElement.style.setProperty('--primary-color', '#0ea5e9');

本地Chrome完美运行。但QA一测iOS 9,直接白屏。查了Can I Use,果然,Safari 9.1才部分支持,且有性能问题。

结论:如果你的用户群包含老旧设备(比如我们有很多三四线城市的中老年用户),这条路走不通。

小插曲:运维大哥看到我在加polyfill,幽幽说:“你这bundle又涨了50KB,CDN费用月底又要超预算了。” 我:……


坑2:引入styled-components,结果打包体积爆了

不甘心,我决定试试CSS-in-JS的代表——styled-components

安装、配置、改写组件:

import styled from 'styled-components';

const Button = styled.button`
  background-color: ${props => props.theme.primary};
  color: white;
`;

// 主题上下文
const theme = {
  primary: '#2563eb'
};

<ThemeProvider theme={theme}>
  <Button>Click me</Button>
</ThemeProvider>

哇!动态换肤?小菜一碟!还能基于props条件渲染样式,再也不用写.btn--disabled这种class了。

但问题很快来了:

  • 打包后体积增加了 87KB(gzip后)
  • 首屏加载慢了近400ms(Lighthouse实测)
  • 运维又来找我:“你们前端是不是又乱装库了?”

更糟的是,服务端渲染(SSR)出现样式闪动——hydration mismatch,因为CSS-in-JS在服务端生成的className和客户端不一致。

我当时真的想砸电脑。宝宝还在哭,Deadline在逼近,而我的页面在深色模式下闪烁如迪厅。


坑3:回归传统CSS Modules,却发现作用域不够“智能”

痛定思痛,我回到相对保守的 CSS Modules

/* Button.module.css */
.primary {
  background-color: var(--color-primary);
}
import styles from './Button.module.css';
<button className={styles.primary}>Click</button>

优点很明显:

  • 作用域隔离,不怕class冲突
  • 构建时生成唯一哈希类名,天然防污染
  • 体积几乎无增加

但问题在于:它还是静态的。虽然可以用CSS变量配合JS切换,但又绕回了兼容性问题。而且,如果要做“根据用户偏好动态计算颜色”(比如运营活动页的渐变背景),CSS Modules力不从心。


真正的解法:按场景拆分策略

经过这一轮折腾,我和技术负责人开了个会(其实是在公司茶水间站着聊的,因为会议室都被产品占了做“脑暴”)。我们达成共识:不要一刀切,而是分层处理

场景 推荐方案 理由
核心产品页(高复用、稳定UI) CSS Modules + 少量CSS变量 体积小、性能高、易维护
动态主题/个性化页面 Emotion(CSS-in-JS) 支持SSR、动态插值、主题切换丝滑
运营活动页(一次性、高定制) 内联style + Tailwind JIT 快速出活,无需长期维护

为什么选Emotion而不是styled-components?两个原因:

  1. 更好的SSR支持:Emotion的@emotion/react配合extractCritical能完美解决hydration问题
  2. 更小的体积:gzip后约12KB,比styled-components小近40%

关键配置如下(Next.js项目):

// _app.js
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import { prefixer } from 'stylis';
import rtlPlugin from 'stylis-plugin-rtl';

const cache = createCache({
  key: 'css',
  stylisPlugins: process.env.NEXT_PUBLIC_RTL === 'true' ? [prefixer, rtlPlugin] : [prefixer],
});

export default function App({ Component, pageProps }) {
  return (
    <CacheProvider value={cache}>
      <Component {...pageProps} />
    </CacheProvider>
  );
}

服务端提取关键CSS:

// _document.js
import { extractCritical } from '@emotion/server';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    const styles = extractCritical(renderPage().html);
    return {
      ...initialProps,
      styles: [
        ...initialProps.styles,
        <style
          data-emotion-css={styles.ids.join(' ')}
          dangerouslySetInnerHTML={{ __html: styles.css }}
        />,
      ],
    };
  }
}

搞定之后,深色模式切换流畅如德芙,Lighthouse性能分重回90+,运维也没再找我谈CDN费用的事(感动)。


面试题挑战:面试官最爱问的3个问题

最近刷面试题,发现“CSS方案选型”成了高频考点。分享几个被问到的问题:

  1. “CSS-in-JS有什么性能问题?”
    答:运行时生成样式(尤其动态props)可能导致重排;但可通过shouldForwardProp、缓存函数等优化。另外,SSR需正确提取样式避免FOUC。

  2. “如何解决CSS-in-JS的调试困难?”
    答:Emotion提供devtool插件,可显示组件对应样式;生产环境开启labelFormat保留组件名,方便定位。

  3. “传统CSS真的过时了吗?”
    答:完全不过时!对于静态、高性能要求的场景(如官网、文档站),原生CSS + PostCSS + PurgeCSS仍是王者。CSS-in-JS是工具,不是信仰。


给带娃程序员的建议:别追求“银弹”

写到这里,宝宝又醒了。赶紧去冲奶粉。

回看这次样式方案之争,最大的感悟是:没有完美的技术,只有合适的场景

如果你在做大促运营页,明天就要上线,那就用内联style + Tailwind,快就是正义;
如果你在维护一个十年老系统,用户都在用IE11,那就老老实实用BEM + Sass;
如果你在做新一代React应用,追求极致体验和开发效率,那CSS-in-JS值得投入。

作为妈妈程序员,我深知时间有多宝贵。与其纠结“哪个更酷”,不如问自己:“哪种能让我不加班,早点回家陪娃?

最后,附上我们项目的最终架构图(文字版):

/src
├── components/          # 通用组件 → CSS Modules
├── pages/
│   ├── product/         # 核心页面 → CSS Modules + CSS变量
│   └── campaign/        # 运营活动 → Emotion (CSS-in-JS)
└── styles/
    ├── variables.css    # 全局CSS变量(仅现代浏览器)
    └── legacy.scss      # 降级样式(针对老旧设备)

希望这篇带娃间隙写的文章,能帮你少踩一个坑,多睡一小时。

共勉。

评论 0

最热最新
暂无评论
协程在摸鱼Lv.1
0
影响力
0
文章
0
粉丝