CSS-in-JS 还是传统 CSS?一个老前端的真心话

代码小镇
2025-06-24 19:10
阅读 634

作为一个在前端领域摸爬滚打了好几年的老兵,我见证了 CSS 从最初的单纯样式定义,到 BEM、OOCSS 等模块化写法的兴起,再到如今 CSS-in-JS 方案的流行。这一路上用过 Sass、Less、PostCSS、Tailwind CSS、Styled Components、Emotion……可以说几乎尝遍了所有主流方案。

今天想和大家聊的是我在实际项目中使用 CSS-in-JS 和传统 CSS 之间的选择与挣扎。这不是一篇理论堆砌的文章,而是结合我们团队在开发一个大型 React 中后台项目时的真实经历。希望通过我的分享,能帮助你少走一些弯路。


一、问题背景:新项目的“样式战争”

一、问题背景:新项目的“样式战争”

去年我们接到一个全新的企业级中后台项目。这个项目有几个显著特点:

  • 多人协作(10+个前端)
  • 模块多、组件复用性高
  • 需要支持多个主题(深色/浅色/品牌定制)
  • 用户交互频繁,UI 状态变化多
  • 上线时间紧张,要求快速迭代

刚开始,我们像往常一样选择了传统的 CSS + BEM 命名方式,配合 SCSS 来组织代码。但在推进过程中发现了不少痛点:

  1. 样式冲突频发:不同页面/组件之间出现重名类名,导致样式覆盖,排查非常费时。
  2. 维护成本高:每次修改样式都要在 .scss 文件和 JSX 文件之间切换。
  3. 主题切换复杂:需要额外封装变量系统,SCSS 变量又不能动态传入,只能靠 JS 注入 CSS 变量,不够优雅。
  4. 状态驱动样式不灵活:React 的 UI 是状态驱动的,但传统 CSS 无法直接根据 state 改变样式,需要写很多条件判断类名。

举个例子:有一个按钮组件,需要根据 isLoadingvariantsize 等属性改变外观。传统写法可能像这样:

// Button.jsx
const Button = ({ isLoading, variant, size }) => {
  const className = `
    button
    ${isLoading ? 'button--loading' : ''}
    ${variant ? `button--${variant}` : ''}
    ${size ? `button--${size}` : ''}
  `;
  
  return <button className={className}>{...}</button>;
}

对应 .scss 文件:

响应式布局概念图-2

.button {
  ...
}

.button--primary {
  background: blue;
}

.button--secondary {
  background: white;
  border: 1px solid gray;
}

/* ... */

这种方式写起来繁琐不说,一旦有新状态加入就得再加一类名,维护起来太累。

这时候就有小伙伴提出:“我们为什么不试试 CSS-in-JS?”——于是我们决定来一场实验。


二、解决方案:引入 Emotion,尝试 CSS-in-JS

二、解决方案:引入 Emotion,尝试 CSS-in-JS

我们最终选用了 Emotion(当时是 11 版本),它既支持 styled 标签也支持 css prop,灵活性很高,而且对 SSR 友好。我们的目标很明确:

  • 解决命名冲突问题
  • 提升组件内样式的可维护性
  • 实现状态驱动样式的能力
  • 快速切换主题

1. 从一个简单的组件开始改造

以刚才提到的按钮为例,我们尝试改造成如下结构:

import styled from '@emotion/styled';

const StyledButton = styled('button')(({ theme, isLoading, variant }) => ({
  padding: '8px 16px',
  borderRadius: '4px',
  fontWeight: 600,
  transition: 'background 0.2s ease',
  cursor: isLoading ? 'default' : 'pointer',
  opacity: isLoading ? 0.7 : 1,


![移动端适配方案-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062419/fefc258f-4912-41b3-ba2c-a1a501390d03.jpg)


  ...(variant === 'primary' && {
    backgroundColor: theme.primaryColor,
    color: '#fff',
  }),

  ...(variant === 'secondary' && {
    backgroundColor: 'transparent',
    border: `2px solid ${theme.secondaryColor}`,
    color: theme.secondaryColor,
  })
}));

写法上是不是清晰了很多?你可以看到:

  • 样式逻辑直接嵌在组件中,所见即所得
  • theme 对象可以从全局注入,实现主题定制
  • 使用对象语法,避免了字符串拼接带来的错误

更妙的是,Emotion 自动生成的 class 是基于哈希值的,几乎不会重复,彻底解决了命名冲突的问题。

2. 主题管理轻松搞定

之前我们用 SCSS 变量控制主题,必须编译后才能生效。而用 CSS-in-JS 后,我们可以通过 React Context 来统一注入主题配置。

// ThemeProvider.jsx
const ThemeContext = React.createContext(defaultTheme);

export const ThemeProvider = ({ children, theme }) => (
  <ThemeContext.Provider value={theme}>
    {children}
  </ThemeContext.Provider>
);

然后在组件内部通过参数拿到:

const MyComponent = styled('div')(({ theme }) => ({
  color: theme.textColor,
  backgroundColor: theme.bgColor
}));

这相当于实现了运行时的主题系统,可以做到用户点击换肤立即生效。

3. 动态样式随心所欲

比如我们要根据当前用户的权限显示不同的操作按钮样式:

const ActionButton = styled('button')(({ role, isActive }) => ({
  backgroundColor: role === 'admin' 
    ? (isActive ? '#d9534f' : '#e9ecef')
    : '#f0ad4e'
}));

再也不用写一堆 conditional classes 了!


三、实践中的挑战与调优

三、实践中的挑战与调优

当然,CSS-in-JS 并非完美无瑕,我们在实践中也遇到了不少坑,下面是一些关键经验教训。

1. 初期学习成本较高

对于习惯了传统写法的新人来说,CSS-in-JS 的写法显得不太直观。特别是:

  • 习惯使用伪类(如 &:hover)的时候,得用对象嵌套:

    {
      '&:hover': {
        backgroundColor: 'red'
      }
    }
    
  • 不再能直接复制浏览器 DevTools 里的 CSS 写法,需要转换成对象格式。

为了应对这个问题,我们做了一个小培训,并整理了一份常用的语法对照表贴在 Wiki 上,帮助大家过渡。

2. 性能优化不容忽视

虽然大部分场景性能差别不大,但我们确实遇到一个卡顿问题。

当时有个表格组件,在每一行都用了 styled 创建临时样式,渲染大量数据时 FPS 明显下降。

解决方法:

  • 将样式提升为静态常量(不在组件内重新计算)
  • 合理使用 useCallback / useMemo
  • 减少不必要的 re-render,利用虚拟滚动(virtual list)

另外,Emotion 默认会在 <head> 插入 style 标签,当组件多时会导致样式标签爆炸。后来我们启用了插件打包 CSS,避免运行时动态插入太多。

3. 调试体验略差

DevTools 展示的 class 名是 hash 值,对调试不太友好。虽然 Emotion 支持 source map,但在生产环境下还是很难定位到具体样式来源。

对策:

  • 开发环境开启 sourcemaps
  • 使用 Chrome 的 "Computed" 面板查看样式合并结果
  • 结合 Chrome DevTools 的快捷键快速跳转到样式面板

4. 构建体积增加

CSS-in-JS 方案相比纯 CSS 更“重”,因为它需要注入运行时逻辑。我们在分析 bundle size 时发现 Emotion 占据了一定比例。

优化策略:

  • 使用 babel 插件提取静态样式,生成独立 CSS 文件(如 @emotion/babel-plugin
  • 开启 CSS Minification
  • 按需加载组件(Code Splitting)

四、效果总结:值得吗?

四、效果总结:值得吗?

说实话,一开始我们也是抱着试试看的心态引入 CSS-in-JS。但从整个项目周期来看,收益远大于成本:

维度 传统 CSS CSS-in-JS
组件隔离性 ❌ 差(依赖BEM等规范) ✅ 强隔离,自动生成hash class
动态样式 ⚠️ 麻烦(类名拼接) ✅ 直接传props控制
主题切换 ⚠️ 编译时变量,无法运行时更改 ✅ 完美支持
协作开发 ⚠️ 依赖严格命名规则 ✅ 更自由,降低命名冲突概率
构建速度 ✅ 快 ⚠️ 稍慢
调试体验 ✅ 好 ⚠️ 需适应

最终我们决定将核心组件库迁移至 Emotion,普通页面保留部分传统 CSS。形成一种混合使用的模式,在合适的场景下使用最合适的工具。


五、给你的建议:别盲目跟风,但也要勇于尝试

如果你正在犹豫是否该采用 CSS-in-JS,这里有一些建议供参考:

✅ 推荐使用的情况:

  • React/Vue 组件库开发:高度封装的组件适合用 CSS-in-JS,增强封装性和可维护性。
  • 状态驱动的 UI:样式频繁跟随 state 变化的组件(如按钮 loading、hover 效果等)非常适合这种写法。
  • 需要运行时主题切换:比 SCSS 变量更灵活。
  • 多人协作大项目:减少命名冲突,提升开发效率。

❌ 不推荐用于:

  • 追求极致首屏性能的小型项目
  • 大量静态页面或营销页
  • 不想增加运行时开销的项目
  • 已经稳定运行的传统项目,强行重构反而得不偿失

🔍 调试与性能小技巧:

  • Chrome DevTools 技巧

    • Esc 打开 console 底部面板,点 Sources 可查看实时生成的 style;
    • 在 Elements 面板中 hover 元素,会自动带出对应的 styled 组件名称。
  • Webpack/Babel 优化

    • 启用 @emotion/babel-plugin 预处理样式为静态 CSS;
    • 生产构建时压缩 CSS;
    • 搭配 eslint-plugin-emotion 检查潜在错误。
  • 开发辅助

    • jest 测试组件样式快照;
    • Storybook 快速预览组件各种状态下的样式表现。

六、最后说点心里话

技术的选择从来都不是非黑即白,每种方案都有其适用的场景和代价。就我个人而言,现在写 React 项目几乎默认都会考虑引入 Emotion 或类似的 CSS-in-JS 方案,特别是在组件粒度细、交互复杂的项目中。

但这并不意味着我要完全抛弃传统 CSS。我会根据不同项目的特点,选择最适合的技术栈。就像开车,有时你需要自动挡,有时手动挡更有掌控感。关键是理解每种工具的优势和限制,而不是盲目追新。

希望这篇来自一线实战的分享,能帮你做出更合适的选择。如果你也在纠结样式方案,不妨像我们当初那样做个 A/B 小实验,亲身体验后再决策。

如果你们团队也在用或者尝试过 CSS-in-JS,欢迎留言交流!咱们一起聊聊踩过的坑,互相学习进步。


文章作者:JerryC,一名热爱编码与写作的全栈开发者,热衷于探索现代前端开发的最佳实践。欢迎关注公众号【前端成长笔记】,获取更多实战干货和技术感悟。

评论 0

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