CSS-in-JS 真的香吗?一个炼丹工程师的样式方案实战复盘

一键启动人生
2025-12-22 11:17
阅读 1618

上周五晚上十一点,我盯着屏幕上那个诡异的样式错位 Bug,手边咖啡早就凉透。产品说“这个改动很简单,就加个 hover 效果”,结果我花了三个小时排查为什么在 Safari 下 :hover 完全不生效——最后发现是某个组件库偷偷用了 CSS-in-JS 的动态类名,而我们的 CSP 策略没放行 style 标签注入。

那一刻我真的想砸电脑。

但冷静下来一想:这其实是个好问题。作为在公司写了三年多前端的老油条(现在主业是调参炼丹,副业还是得写 React),我越来越觉得,现代前端项目里样式方案的选择,已经不是“用不用 SCSS”那么简单的事了。尤其最近被领导暗示“要考虑新机会”,面试时好几个团队都问:“你们项目用的是 styled-components 还是 emotion?为什么?”

行吧,今天就来聊聊这个话题。顺便也给自己理理思路,毕竟下份工作大概率还得和 React 打交道。


一场由 Go 微服务引发的样式混乱

事情得从去年说起。我们团队原本是个纯前端组,后来公司搞微前端架构,后端用 Go 写了一堆独立服务,每个服务配一个轻量级前端界面。为了统一体验,我们抽了个公共 UI 库,用传统 CSS + BEM 命名规范,配合 PostCSS 和 Autoprefixer,跑得挺稳。

但问题来了:Go 服务的前端团队(其实是后端工程师兼职写的)根本不想学 BEM,他们直接在 React 组件里写 <div style={{ color: 'red' }}>,美其名曰“简单直接”。结果上线后,按钮颜色五花八门,连字体大小都不统一。更离谱的是,有个服务为了“快速上线”,直接把整个 Ant Design 的 CSS 全量引入,导致我们主应用的全局样式被污染。

产品经理在站会上阴阳怪气:“你们前端是不是对‘一致性’有什么误解?”

于是技术债爆雷,老板拍板:重构样式系统,必须支持按需加载、作用域隔离、主题切换。Deadline 是双11前两周。

压力山大。


CSS-in-JS:救星还是新坑?

当时摆在面前的选项不多:

  • 继续用传统 CSS,但加 Scoped CSS(比如 Vue 的 scoped 或 Webpack 的 CSS Modules)
  • 全面拥抱 CSS-in-JS,比如 styled-components / emotion
  • 折中:用 Tailwind CSS 这类原子化方案

我们先试了 CSS Modules。配置不难,在 webpack.config.js 里加几行就行:

{
  test: /\.module\.css$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        modules: {
          localIdentName: '[name]__[local]___[hash:base64:5]'
        }
      }
    }
  ]
}

本地开发没问题,但部署到测试环境后,发现两个微前端应用如果用了同名的 CSS Module 文件(比如都叫 Button.module.css),生成的类名居然会冲突!因为 hash 是基于文件路径的,而不同 Git 仓库的路径结构相似。

翻了 Stack Overflow 才知道,得手动传入 context 参数确保唯一性。但 Go 团队那帮人哪愿意改构建配置?他们连 package.json 都懒得碰。

那就试试 CSS-in-JS 吧。

emotion 上手确实快。写个带主题的按钮:

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

const buttonStyle = (theme) => css`
  padding: 8px 16px;
  border-radius: 4px;
  background: ${theme.primary};
  color: white;
  &:hover {
    opacity: 0.9;
  }
`;

const Button = ({ children }) => (
  <button css={buttonStyle}> {children} </button>
);

爽!作用域天然隔离,主题切换一行代码搞定,还能利用 JS 的逻辑能力做动态样式(比如根据 props 改变 padding)。而且 emotion 支持 SSR,对 SEO 友好——这点比早期 styled-components 强。

但很快又踩坑了。

坑一:性能问题
emotion 默认会在运行时生成 <style> 标签插入 <head>。当页面有上百个组件时,DOM 操作频繁,Chrome DevTools 里能看到明显的 Layout Thrashing。我们压测时发现首屏 FCP(First Contentful Paint)慢了 300ms。

解决方案是启用 @emotion/server 做 SSR 提取,或者用 @emotion/cache 手动管理样式插入点。但这就要求所有微前端入口都得改造,Go 团队又炸了。

坑二:调试体验差
传统 CSS 的类名是固定的(比如 .btn-primary),浏览器 Elements 面板里一看就知道是哪个组件。但 CSS-in-JS 生成的类名是哈希(比如 css-1a2b3c),除非装插件(如 Styled Components DevTools),否则根本没法调试。有一次线上样式错乱,测试甩过来截图,我对着一堆 css-xyz 类名硬是猜了半小时。

坑三:CSP 兼容性
回到开头那个 Safari 的事故。很多企业内网环境启用了严格的 Content Security Policy,禁止内联 style。CSS-in-JS 动态插入 <style> 标签会被浏览器拦截,导致样式完全失效。虽然可以通过 nonce 或 hash 白名单解决,但运维又得改 Nginx 配置——他们看到我就躲。


我们最终的选择:混合策略

折腾一个月后,我们定了个“务实主义”方案:

场景 方案 理由
公共 UI 库(按钮、表单等) 传统 CSS + CSS Modules 稳定、可缓存、CSP 友好、调试方便
动态主题/复杂交互组件 emotion(仅限 runtime) 利用 JS 逻辑处理状态驱动样式
快速原型/MVP Tailwind CSS(utility-first) Go 工程师也能快速上手,减少定制样式

具体落地时做了几件事:

  1. 强制约定:所有公共组件必须用 .module.css,且类名前缀为组件名(如 Button__root
  2. 封装 emotion hook:提供 useDynamicStyle,内部处理 SSR 和 CSP 适配
  3. 禁用内联 style:ESLint 加规则,禁止 style={{}} 写法
  4. 构建时提取:用 @emotion/babel-plugin 把静态样式转成 class,减少运行时开销

效果立竿见影。双11 当天零样式相关告警,产品经理居然在群里夸了句“这次体验很一致”——要知道他平时只说“这个要改,那个要改”。


一点思考:技术选型不是非黑即白

很多人讨论 CSS-in-JS vs 传统 CSS,总喜欢站队。但真实项目哪有那么多“银弹”?作为天天和 deadline 赛跑的工程师,我只关心三件事:

  • 能不能让 Go 工程师少找我麻烦?
  • 线上会不会半夜报警?
  • 跳槽面试官会不会觉得我 out of date?

CSS-in-JS 在动态性、组件化上确实强,但它把样式逻辑和 JS 捆绑,牺牲了可缓存性、可调试性和安全性。而传统 CSS 虽然“老派”,但在工程化、性能、兼容性上依然稳如老狗。

最近我在研究 Rust,发现它有个理念特别对味:零成本抽象(Zero-cost abstractions)。意思是,高级特性不该带来运行时开销。反观某些 CSS-in-JS 库,为了“开发体验”默默吃掉几百毫秒,这在用户体验至上的今天,真的值得吗?

所以我的建议是:

  • 如果你的应用是内容型(博客、文档站),用传统 CSS + utility class(如 Tailwind)就够了
  • 如果是高度交互的后台系统(比如数据看板、设计器),可以局部用 CSS-in-JS 处理复杂状态
  • 别为了“技术潮流”全盘切换,先问问运维和测试的意见

最后:别让样式成为你跳槽的绊脚石

写这篇文章时,我刚拒了一个 offer——对方技术栈全是 styled-components + Next.js,但他们的 CSP 策略极其严格,又不肯用 nonce。我问“那怎么处理动态样式?”,CTO 说“我们一般不用动态样式”。……行吧。

现在找工作,光会 React 不够了,得懂工程化、懂性能、懂协作成本。样式方案看似小事,实则牵一发动全身。下次面试被问到这个问题,别只说“我用 emotion,因为好用”,试着聊聊 CSP、SSR、微前端隔离、团队协作成本——这些才是 senior 工程师该考虑的。

哦对了,如果你也在纠结样式方案,不妨试试我最近整理的 checklist:

  • 是否需要支持严格的 CSP?
  • 团队里有没有非前端成员要写样式?
  • 应用是否需要 SSR / SSG?
  • 主题切换是编译时还是运行时需求?
  • 能接受多少额外的 bundle size?

答案不同,选择自然不同。

好了,咖啡喝完了,该去调下一个模型参数了。希望这篇碎碎念能帮到你——至少下次遇到 Safari 的 :hover 失效,别像我一样想砸电脑。

评论 0

最热最新
暂无评论
一键启动人生Lv.1
0
影响力
0
文章
0
粉丝