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

写给机器的诗
2025-12-16 03:29
阅读 277

上周五晚上十点半,办公室只剩我和隔壁工位的运维小哥还在“坚守阵地”。他忙着处理凌晨大促的服务器扩容,我则对着一个诡异的样式冲突抓耳挠腮——某个按钮在 Chrome 正常,但在 Safari 上莫名其妙地多了一层阴影。产品经理明天一早就要 demo,而我连问题出在哪都没搞清楚。

那一刻,我真的想砸电脑。

不过说来也巧,就在我疯狂 console.log + !important 的时候,耳机里刚好播到 Radiohead 的《No Surprises》,歌词里那句 “a heart that’s full up like a landfill” 突然让我顿悟:我的 CSS 文件,不就是个填满了过期样式的垃圾场吗?

作为一个在同一家公司待了三年多、至今仍坚持手写每一行代码的老派前端(是的,我连 VS Code 的自动补全都关了,纯粹是强迫症),我最近却开始被迫接触一些“新玩意儿”——比如 AI 辅助编程,还有今天要聊的主角:CSS-in-JS

为什么?因为领导在 Q2 技术规划会上拍板:“咱们得拥抱现代化,别老用十年前那一套。” 言下之意:你这老古董也该更新一下技术栈了。再加上我自己也在偷偷刷 LeetCode、看 Hugging Face 文档,准备年底跳槽换个环境——毕竟,谁不想在简历上写点“React + Emotion + TypeScript”的高大上组合呢?

所以,今天这篇不是什么权威教程,就是一个保守派前端在真实项目里踩坑、试错、甚至被线上 bug 打脸后的一点开发心得。如果你也正纠结于该用传统的 .css 文件还是时髦的 CSS-in-JS,希望我的经历能帮你少走点弯路。


一切的起点:那个“全局污染”的噩梦

先说说我们公司的技术栈吧。主站是 React + Webpack + Less,样式基本靠 BEM 命名规范撑着。听起来挺标准?但现实很骨感。

去年双11前,我们接了个紧急需求:给商品详情页加个“限时秒杀”弹窗。时间紧任务重,我随手建了个 FlashSaleModal.less,写了几十行样式。结果上线第二天,测试小姐姐一脸严肃地找我:“首页的轮播图怎么变窄了?”

我一脸懵,查了半天才发现——我在 .modal-container 里用了 width: 80%,而首页轮播图的类名恰好叫 .container。虽然按理说作用域不同,但由于 Webpack 的 chunk 拆分策略问题,这两个文件被打包进了同一个 CSS bundle,导致样式互相污染。

那一刻我才意识到:传统 CSS 的“全局性”在大型项目里就是一颗定时炸弹

当然,你可以用 CSS Modules 或者 Scoped CSS 来解决。我们也试过,但团队里新人太多,有人忘了加 :global,有人把类名拼错,还有人直接在组件里写内联 style……久而久之,代码库变得像一锅乱炖。

于是,我开始认真研究 CSS-in-JS


初探 CSS-in-JS:真香还是智商税?

第一次听说 CSS-in-JS 是在 GitHub 上逛某个开源项目时。看到人家用 styled-components 写组件,样式和逻辑紧耦合,还能动态传 props 改样式,我当时内心 OS:“这不就是把 CSS 塞进 JS 里搞事情吗?性能能好吗?”

但好奇心害死猫。我本地新建了个小项目,装了 @emotion/react(为啥选 Emotion?因为 Next.js 官方推荐,而且据说比 styled-components 轻量),写了个简单的按钮:

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

const primaryButton = (theme) => css`
  background-color: ${theme.colors.primary};
  border: none;
  padding: 12px 24px;
  border-radius: 8px;
  color: white;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s ease;

  &:hover {
    background-color: ${theme.colors.primaryDark};
  }
`;

export default function Button({ children, variant = 'primary' }) {
  return (
    <button css={variant === 'primary' ? primaryAssistant : secondaryStyle}>
      {children}
    </button>
  );
}

跑起来一看——哇,样式完全隔离!就算我在十个组件里都叫 button,也不会互相干扰。更爽的是,我可以直接用 JS 变量控制颜色、间距,甚至根据 props 动态切换主题。

再也不用担心全局污染了!

但很快,问题来了。

性能焦虑:运行时生成样式真的好吗?

我拿 Chrome DevTools 的 Performance 面板测了一下,发现每次组件渲染,Emotion 都会动态生成 <style> 标签插入 <head>。虽然它有缓存机制(相同的样式只生成一次),但在 SSR 场景下,首屏 HTML 里会塞满内联样式,体积暴涨。

有一次我给一个列表页加了 50 个带 hover 效果的卡片,每个卡片都有独立的 CSS-in-JS 样式。结果 Lighthouse 分数从 90+ 掉到 60,FCP(First Contentful Paint)慢了近 1 秒。运维小哥直接在群里 at 我:“兄弟,你这页面是不是在挖矿?”

后来查文档才知道,Emotion 提供了 @emotion/cssextractCritical 这种 SSR 优化方案,但配置起来比传统 CSS 复杂多了。对于习惯了 import './style.css' 的我来说,简直像从自行车换到了战斗机——功能强,但操作手册厚。


传统 CSS 的“回光返照”:Tailwind 是救星吗?

就在我对 CSS-in-JS 产生动摇时,隔壁组开始推 Tailwind CSS

说实话,一开始我是拒绝的。看着满屏的 className="flex justify-between items-center p-4 bg-gray-100 rounded-lg",我觉得这简直是语义化的倒退。但架不住人家构建快、体积小、还能 Purge 无用样式。

我试着在一个新项目里用 Tailwind + 传统 CSS 混搭:布局用 Tailwind,定制化样式(比如动画、复杂交互)写在单独的 .module.css 里。

效果出乎意料地好。

  • 构建速度飞快(Vite + Tailwind 几乎秒开)
  • 样式体积压缩后只有 8KB
  • 团队新人上手极快,毕竟 class 名都是语义化的英文单词

但问题也明显:当 UI 需求高度定制时,Tailwind 的 utility-first 反而成了束缚。比如产品经理突然说:“这个按钮的 hover 效果要加个波纹扩散动画”,你只能回到传统 CSS 写 keyframes,或者硬凑十几个 animate-xxx 类——那画面太美我不敢看。


综合对比:没有银弹,只有权衡

经过几个月的折腾,我整理了一份粗略的对比表(基于我们团队的真实项目数据):

维度 传统 CSS / CSS Modules CSS-in-JS (Emotion) Tailwind CSS
作用域隔离 需手动管理(BEM/Modules) 自动隔离,天然组件化 依赖 class 命名,需规范
动态样式 需用 CSS 变量或 JS 操作 DOM 直接传 props,非常灵活 需配合 style 属性或自定义插件
SSR 支持 原生支持,简单直接 需额外配置(如 extractCritical 原生支持,PurgeCSS 自动优化
构建性能 快(尤其 Vite) 中等(运行时注入) 极快(按需生成)
Bundle 体积 小(可 Tree Shaking) 中(运行时库 ~10KB) 极小(Purge 后 <10KB)
学习成本 低(前端必修课) 中(需理解 JS 闭包、缓存) 低(但需适应 utility 思维)
调试体验 DevTools 直接编辑 样式在 <style> 标签里,难定位 class 太多,HTML 很臃肿

注:数据基于 React 18 + Vite 3 + Emotion 11 的实测环境

从这张表能看出什么?没有哪一种方案是完美的。关键看你项目的规模、团队的技术栈、以及对“开发体验”和“运行时性能”的优先级排序。


我的最终选择:混合策略 + 渐进式改造

现在,我在新项目里采用了一种“混合策略”:

  • 基础布局 & 常用组件:用 Tailwind 快速搭建,保证一致性
  • 复杂交互 & 动画:用 Emotion 写 CSS-in-JS,利用其动态能力
  • 遗留模块:继续用 CSS Modules,逐步重构,不搞一刀切

比如一个商品卡片组件:

// ProductCard.jsx
import { css } from '@emotion/react';
import clsx from 'clsx';

const cardHoverEffect = css`
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  &:hover {
    transform: translateY(-4px);
    box-shadow: 0 10px 25px rgba(0,0,0,0.15);
  }
`;

export default function ProductCard({ product, isPromoted }) {
  return (
    <div 
      className={clsx(
        "p-4 bg-white rounded-xl shadow-sm", // Tailwind 基础样式
        { "ring-2 ring-yellow-400": isPromoted } // 条件样式
      )}
      css={cardHoverEffect} // 复杂交互用 Emotion
    >
      <h3 className="font-bold text-lg">{product.name}</h3>
      {/* ... */}
    </div>
  );
}

这样既享受了 Tailwind 的开发效率,又保留了 CSS-in-JS 的灵活性。而且,因为 Emotion 只用在少数需要动态样式的组件上,整体 bundle 体积和性能影响几乎可以忽略。


一点真诚的建议:别为技术而技术

写到这里,我想起刚入行时的一个教训。那时为了炫技,非要在项目里用 SASS 的 mixin 和 function 搞一套“设计系统”,结果维护成本高到爆炸,最后被 leader 骂:“你写的代码,除了你自己没人看得懂。”

现在回头看,技术选型的核心不是“新不新”,而是“适不适合”

  • 如果你是一个 3 人小团队,快速迭代 MVP,Tailwind + 传统 CSS 足够;
  • 如果你在做高度定制的 Design System 或主题切换应用,CSS-in-JS 的动态能力会省下大量 hack;
  • 如果你在维护一个 5 年以上的老项目,别急着全盘替换,用 CSS Modules 逐步隔离更稳妥。

顺便提一句,我在 GitHub 上看了很多开源项目的样式方案,发现连 React 官方文档站都用的是传统 CSS + CSS Modules,而像 Vercel、Shopify 这类现代平台则大量使用 CSS-in-JS。这说明什么?大厂也在根据场景做选择,而不是盲目跟风


结语:保守派的“妥协”与成长

作为一个习惯边听 Radiohead 写代码、至今还手敲每行 CSS 的“老顽固”,我承认自己曾经对 CSS-in-JS 有偏见。但现实教育了我:前端的世界没有永恒的最佳实践,只有不断演进的解决方案

现在,我会在合适的场景用 Emotion,也会毫不犹豫地写 .module.css,甚至偶尔用内联 style(别 judge 我,紧急修复线上 bug 时真香)。AI 辅助工具我也开始试了——虽然大部分时候只是用来生成重复的 boilerplate,但至少能让我多喝两口咖啡。

如果你也在考虑跳槽、学新技术,我的建议是:先搞懂原理,再决定用不用。GitHub 上有无数 demo 和最佳实践,多 clone 下来看看,别光听别人吹。

最后,分享一句我贴在显示器边的话:“Don’t rewrite, refactor.” —— 别想着推倒重来,学会在现有基础上渐进优化,才是工程师的成熟之道。

好了,音乐播到下一首了,是 Arcade Fire 的《Ready to Start》。我得去改那个 Safari 的阴影 bug 了——这次,我决定用 CSS 变量 + @supports 来兼容,而不是 !important

祝大家都能写出干净、高效、不让人半夜惊醒的样式代码。

评论 0

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