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

Web技术
2025-12-16 05:29
阅读 269

大家好,我是阿哲。一个在北京中关村附近创业公司搬砖的全栈开发——说白了就是那种“前端后端都得会、数据库崩溃了也得修、连产品经理画的原型图歪了都要我调”的打杂型程序员。每天通勤一小时(回龙观→国贸,懂的都懂),晚上回家还得研究点新东西,不然怕被卷死。

上个月我们团队搞双11大促活动页重构,产品那边突然丢来一句:“要丝滑动效,还要主题可换,最好还能按用户分组展示不同颜色……” 我当时就坐在工位上盯着屏幕,心里默默问候了一遍祖宗十八代。但骂归骂,活还得干。于是我就不得不重新思考:到底该用传统的 CSS 文件,还是拥抱 CSS-in-JS?

这篇文章不是什么高深理论,纯粹是我在项目实战中踩坑、翻车、又爬起来后的血泪总结。如果你也在纠结 React 项目里怎么写样式更优雅、更高效、更不容易被测试同学提一堆 UI 偏移 bug,那这篇教程或许能帮你少走点弯路。


一切的开始:那个“简单”的需求

事情是这样的。我们有个营销页面,原本用的是普通的 .scss + BEM 命名规范,结构清晰、编译快、浏览器兼容性稳如老狗。但这次产品提的需求有点离谱:

  • 主题色要根据用户来源渠道动态切换(比如微信进来的用红色,抖音进来的用紫色)
  • 某些区域要有 hover 动画 + 微交互动效(别小看这个,做不好就卡成 PPT)
  • 后期可能还要支持暗黑模式
  • 最关键的是:下周三上线!

我第一反应是:“这不就是加几个 CSS 变量的事?”
结果刚改完,测试同学跑来:“阿哲,iOS 12 上动画卡顿,而且有些按钮颜色没变。”
我:“……行吧。”

这时候我意识到:传统 CSS 在动态性和 JS 集成度上的短板,开始暴露了。


传统 CSS 的优点:稳、快、省心

先别急着喷传统 CSS。说实话,在很多场景下,它依然是王者。

我们团队之前所有后台管理系统、数据看板,清一色用 SCSS + PostCSS + CSS Modules。为啥?因为:

  • 构建速度快:Webpack 打包时样式文件单独处理,不拖慢 JS bundle。
  • 浏览器原生支持:没有运行时开销,首屏渲染快到飞起。
  • 调试方便:DevTools 里直接看到类名,改个颜色立马生效。
  • 团队协作友好:设计师给 Figma,前端对照写类,互不干扰。

特别是我们这种小公司,人手紧、迭代快,谁有时间天天折腾 fancy 的新方案?能跑就行。

举个真实例子:去年我们搞一个内部运维面板,用纯 CSS Grid + Flex 布局,三天搞定,上线零样式 bug。测试同学甚至夸我:“这次居然没偏移!”

但问题来了——一旦需要和 JavaScript 深度联动,传统 CSS 就显得力不从心。

比如动态主题?你得靠 JS 去 document.documentElement.style.setProperty('--primary-color', color),再配合大量 CSS 变量。逻辑分散在两个文件里,维护起来像在玩“找不同”。

再比如条件样式:user.isVip ? 'button-vip' : 'button-normal' —— 看似简单,但类名越来越多,最后 .button-vip-special-limited-edition 这种名字都快出来了,BEM 直接破防。


CSS-in-JS:把样式塞进 JS 的“邪道”

于是我把目光投向了 CSS-in-JS。主流方案有 Styled Components、Emotion、Linaria 等。我们选了 Emotion,因为它支持 css prop、性能好、还能和 SSR 完美配合(我们用 Next.js)。

为什么没选 Styled Components?因为 bundle size 大了一点点,而我们对首屏加载速度极其敏感(产品说“用户多等 100ms 就跑了”——虽然我觉得他在吹牛)。

用 Emotion 写样式大概是这样的:

import { css } from '@emotion/react'

const Button = ({ variant, children }) => {
  return (
    <button
      css={css`
        padding: 8px 16px;
        border-radius: 4px;
        background: ${variant === 'primary' ? '#3b82f6' : '#e5e7eb'};
        color: ${variant === 'primary' ? 'white' : 'black'};
        transition: all 0.2s ease;

        &:hover {
          transform: translateY(-2px);
          box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        }
      `}
    >
      {children}
    </button>
  )
}

看起来是不是很爽?样式和组件逻辑绑在一起,变量直接从 props 拿,不用再翻半天 SCSS 文件。

更绝的是动态主题:

// theme.js
export const theme = {
  colors: {
    primary: '#3b82f6',
    secondary: '#10b981'
  }
}

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

const ThemedButton = () => {
  const theme = useTheme()
  return <button css={{ backgroundColor: theme.colors.primary }} />
}

一行代码搞定主题切换,再也不用手动 setProperty。

而且 Emotion 支持 自动 vendor prefixcritical CSS 提取(SSR 场景下只注入当前页面用到的样式),甚至还能 避免类名冲突——因为它生成的是哈希类名,比如 css-1a2b3c,根本不怕全局污染。


踩坑实录:不是所有光鲜都无代价

但别急着欢呼。CSS-in-JS 也不是银弹。我在双11项目里至少踩了三个大坑。

坑一:首屏性能差点翻车

上线前一天压测,发现 Lighthouse 分数掉了 10 分。排查发现:Emotion 在客户端注入样式是异步的,导致首屏出现 FOUC(Flash of Unstyled Content)——按钮先是默认样式,0.3 秒后才变蓝。

解决方案:必须配合 SSR 把 critical CSS 内联到 <head>。Emotion 官方文档其实写了,但我一开始偷懒没配,结果被运维大哥追着问:“你们前端是不是又搞什么花里胡哨的东西?”

配完 SSR 后:

// _app.js (Next.js)
import { CacheProvider } from '@emotion/react'
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'

const cache = createCache({ key: 'css' })
const { extractCritical } = createEmotionServer(cache)

function MyApp({ Component, pageProps, emotionStyleTags }) {
  return (
    <CachePRovider value={cache}>
      <head>
        {emotionStyleTags}
      </head>
      <Component {...pageProps} />
    </CacheProvider>
  )
}

MyApp.getInitialProps = async (ctx) => {
  const initialProps = await App.getInitialProps(ctx)
  const styles = extractCritical(renderPage().html)
  return {
    ...initialProps,
    emotionStyleTags: styles.css.map(style => (
      <style
        key={style.key}
        dangerouslySetInnerHTML={{ __html: style.css }}
        data-emotion={style.key}
      />
    ))
  }
}

搞定。FOUC 消失,Lighthouse 分数回来了。但说实话,配置成本比纯 CSS 高多了

坑二:调试体验降级

以前 DevTools 里看到 .btn-primary,现在看到 css-1x2y3z。虽然 Emotion 插件能显示源码位置,但断点调试样式?不存在的

有一次动画卡顿,我硬是花了两小时才定位到是某个 transform 触发了 layout thrashing。要是用传统 CSS,直接在 Sources 里改值试效果,分分钟搞定。

坑三:bundle size 膨胀

虽然 Emotion 运行时很小(~5KB gzipped),但每个组件都带样式字符串,JS bundle 还是变大了。我们监控发现,首页 JS 体积增加了 12%。

后来我们做了 code splitting + 懒加载非首屏组件,才勉强压回去。但这也意味着:CSS-in-JS 更适合 SPA 或复杂交互应用,不适合内容型、SEO 优先的页面。


对比表格:一目了然

为了帮自己理清思路,我做了个对比表(顺便分享给你们):

维度 传统 CSS (SCSS + Modules) CSS-in-JS (Emotion)
动态样式支持 弱(依赖 CSS 变量或 class 切换) 强(直接使用 JS 变量)
主题切换 需手动管理 CSS 变量 内置 ThemeProvider,一行切换
作用域隔离 靠命名规范或 Modules 自动哈希类名,天然隔离
构建速度 快(样式独立编译) 稍慢(需处理 JS 中的样式)
运行时性能 无开销 少量(但可忽略)
SSR 支持 天然支持 需额外配置 critical CSS
调试体验 极佳(DevTools 直接编辑) 一般(需插件,类名不可读)
Bundle Size 样式单独提取,JS 小 样式嵌入 JS,体积略增
学习成本 低(所有前端都会) 中(需理解新 API)
适合场景 静态页面、后台系统、内容站 动态 UI、复杂交互、设计系统

我的最终选择:混合策略

经过这次双11血战,我悟了:没有最好的方案,只有最合适的组合。

我们现在采用 混合策略(Hybrid Approach)

  • 基础布局、通用组件(按钮、输入框) → 用传统 SCSS + CSS Modules,保证稳定和性能。
  • 高度动态、交互复杂的模块(比如活动页、可视化图表) → 用 Emotion,享受 JS 驱动样式的便利。
  • 全局主题变量 → 用 CSS 变量定义,同时在 Emotion 的 theme 里同步一份,两边都能用。

比如:

/* globals.css */
:root {
  --color-primary: #3b82f6;
}
// theme.js
export const theme = {
  colors: {
    primary: 'var(--color-primary)' // 兜底,也可直接写死
  }
}

这样,即使某个组件还没迁移到 Emotion,也能读到主题色。


给 React 开发者的建议

如果你正在用 React,纠结要不要上 CSS-in-JS,我的建议是:

  1. 别为了用而用。如果你的项目是博客、电商详情页、CMS,老老实实用传统 CSS,省心省力。
  2. 如果是复杂交互应用(比如设计器、仪表盘、游戏化界面),CSS-in-JS 能极大提升开发体验。
  3. 务必配好 SSR,否则 FOUC 会让你在上线前夜失眠。
  4. 不要全盘迁移。可以先在一个新模块试点,跑通流程再推广。
  5. 关注 bundle size 和 Lighthouse 分数,别让“炫技”拖垮用户体验。

结语:技术没有银弹,只有权衡

写这篇文章的时候,已经是凌晨一点。窗外北京的夜色很静,只有键盘敲击声。回想上周五晚上加班到十点,终于把主题切换 bug 修好,那一刻真的想请自己喝杯奶茶。

作为创业公司的全栈,我深知:技术选型不是比谁用的工具最新,而是谁能用最稳的方式按时交付。

CSS-in-JS 是一把锋利的刀,能切出精致的 UI,但也可能割伤自己。传统 CSS 是把钝斧,笨重但可靠。关键看你砍的是什么木头。

希望这篇带点自嘲、带点干货的教程,能帮你少熬几个夜,少砸几台电脑。

毕竟,程序员的命也是命,对吧?

—— 阿哲,于北京回龙观出租屋,2024年6月

评论 0

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