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

红黑树下乘凉
2025-06-29 00:58
阅读 502

从“样式战争”到“各司其职”:我在前端样式方案上的选择与思考

从“样式战争”到“各司其职”:我在前端样式方案上的选择与思考

我第一次听到“CSS-in-JS”的时候,内心是抗拒的。

彼时我正负责一个中型 React 项目的重构。项目初期我们还是用的传统 CSS + SCSS 预处理器,配合 BEM 命名规范来组织代码结构。但随着功能迭代、模块越来越多、团队新人不断加入,我们开始遭遇一系列令人头疼的问题:

  • 类名冲突导致样式互相污染
  • 全局样式覆盖难以追踪
  • 业务逻辑与样式强耦合后维护成本陡增
  • 复杂组件的状态变体(hover、focus、disabled)管理混乱

那段时间每天都要在开发者工具里翻来覆去查元素、看 computed styles,和产品经理拉扯各种“为什么按钮会变成红色?”这类问题。更糟的是,有些样式 Bug 居然是因为第三方组件库和自定义样式产生了意外交互——这简直是灾难现场。

于是我开始认真研究起当时社区热议的 CSS-in-JS 方案,想看看能不能找到更好的解法。


项目背景:一次复杂的后台系统重构

我们的项目是一个面向企业用户的 SaaS 平台后台管理系统,包含用户管理、权限配置、数据可视化等多个核心模块,技术栈主要是 React + TypeScript + Ant Design。

原本的架构采用传统方式:

components/
  Button/
    index.jsx
    style.scss
containers/
  Dashboard/
    index.jsx
    style.scss
globalStyles/
  _reset.scss
  _variables.scss

每个组件都有自己独立的样式文件,在开发初期一切井然有序。然而当团队人数从原来的 3 人增加到 7 人后,问题接踵而至:

  1. 类名冲突 —— DashboardUserList 同时用了 .title
  2. 样式全局污染 —— 某个同事在 globalStyles 加了一个通用的 .table-row 样式,结果影响了所有表格组件
  3. 样式复用困难 —— 想把一个组件的样式拆出来给另一个使用?得手动 copy-paste、重命名、调整变量,效率极低
  4. 调试复杂度上升 —— 开发者工具里显示的类名是 .style__title__2356,但源码却分布在多个文件中

前端性能优化图表-1


转折点:尝试引入 styled-components

经过技术选型后,我们决定试点引入 styled-components,并选择其中几个关键组件进行重构。改造前后的差异非常直观,比如:

传统写法:

// components/Button/index.jsx
import './Button.scss';

const Button = ({ children, onClick }) => (
  <button className="btn" onClick={onClick}>
    {children}
  </button>
);
/* components/Button/Button.scss */
.btn {
  background-color: #007BFF;
  color: white;
  &:hover {
    background-color: #0069D9;
  }
}

改造为 styled-components 写法:

// components/Button/index.jsx
import styled from 'styled-components';

const StyledButton = styled.button`
  background-color: #007BFF;
  color: white;

  &:hover {
    background-color: #0069D9;
  }
`;

const Button = ({ children, onClick }) => (
  <StyledButton onClick={onClick}>{children}</StyledButton>
);

这个改动看似不大,但在实际开发过程中带来了几个显著的变化:

  1. 组件样式内聚性提升:样式定义就在 JS 文件内部,不需要来回切换文件查找样式来源
  2. 避免命名冲突:通过动态生成唯一的 class 名解决类名重复问题
  3. 支持 props 动态注入样式:可以轻松实现根据状态变换样式的场景

举个例子,我们需要一个按钮根据不同类型展示不同颜色:

const StyledButton = styled.button`
  background-color: ${props => props.primary ? '#007BFF' : 'gray'};
  color: white;
`;

这样直接通过组件 props 控制样式,完全无需再写多个类名组合判断。


实际挑战与性能考量

尽管 CSS-in-JS 看起来很美好,但我们在实践中也遇到不少问题,尤其是在首次引入阶段。

1. 首屏渲染慢 & FOUC 问题

最明显的就是页面加载瞬间偶尔会出现“样式未应用”的内容闪烁(Flash of Unstyled Content)。排查下来发现原因有两个:

  • styled-components 使用服务端渲染(SSR)时没有正确提取静态样式
  • 客户端初次挂载时动态注入样式需要时间

解决方案:

  • 引入 @emotion/styled 替代部分 styled-components,因为它对 SSR 友好程度更高
  • 在 Next.js 项目中启用 getServerSideProps 提取静态样式
  • 增加骨架屏或 loading 占位符优化用户体验

2. 可维护性 vs 性能的权衡

虽然 CSS-in-JS 的封装很好,但随着组件增多,我们发现:

  • 组件内部样式越来越复杂,代码臃肿
  • 调试时浏览器开发者工具显示的 class 名是一串 hash,不利于排查

最终做法:

  • 对于基础组件库(如 Button、Input),继续使用 styled-components
  • 对于业务组件或容器级组件,回归传统 CSS Modules 写法 + SCSS 变量管理
  • 使用 postcss-prefixwrap 为全局样式自动加上前缀防止污染

3. 构建产物体积增长

使用 CSS-in-JS 后,打包体积明显变大。尤其是项目中很多组件其实共享了一组主题色、字体大小等变量,但我们把它们都写进了各自的 styled 组件中,而不是统一引用设计变量。

后来我们做了两个优化:

  • 抽离出 theme.ts 统一管理设计 token
  • 所有样式尽量基于 theme 中的变量编写
// theme.ts
export const colors = {
  primary: '#007BFF',
  danger: '#DC3545',
};
import styled from 'styled-components';
import { colors } from '../../theme';

const StyledButton = styled.button`
  background-color: ${colors.primary};
`;

最终效果与收益总结

整个过渡过程持续了大约两个月,从最初的全盘拒绝,到最后我们逐步摸索出了一个混合使用的最佳实践路径。最终的效果体现在以下几个方面:

指标 改造前 改造后
类名冲突问题 高频出现 几乎绝迹
样式复用难度 高(依赖 SCSS mixin) 中等(组件抽象 + 主题变量)
开发体验 需频繁切换文件 更流畅的一体化编码
构建性能 中等 小幅下降
首屏加载体验 偶尔闪现 优化后稳定

特别值得一说的是团队协作体验的提升。之前经常出现“你改了一个 SCSS 文件导致五个组件出问题”,而现在每个人只负责自己的组件样式,几乎不再出现这种牵一发动全身的 Bug。


我的经验建议:别站队,看场景

现在回头看,我觉得当初争论“CSS-in-JS 好不好”这件事本身就很片面。真正重要的是理解每种方案背后的设计理念,并根据项目实际情况做出取舍。

以下是我的几点实战建议:

✅ 适合使用 CSS-in-JS 的情况:

  • 组件化程度高、追求样式封闭性的项目(如设计系统、UI 库)
  • 需要根据组件状态动态生成样式的场景
  • 团队规模大,多人协作容易产生类名冲突

推荐方案:

✅ 传统 CSS/SCSS 依旧适用的场景:

  • 页面级布局和全局样式
  • 追求极致性能的小型项目
  • 需要兼容老旧浏览器(如 IE11)

推荐搭配:

  • CSS Modules 或 SCSS Modules
  • postcss + autoprefixer 自动加厂商前缀
  • purgeCSS 清理未使用 CSS

🚫 不建议的场景:

  • 所有样式都一股脑塞进 styled 组件中 → 导致 JS 包体积暴涨
  • 忽视样式复用 → 失去了设计系统的优势
  • 不做性能监控 → 没有意识到 CSS-in-JS 的副作用

工具推荐与调试小技巧

在使用过程中,我发现一些工具和技巧大大提升了效率:

  • React Developer Tools:可以查看某个元素对应的 styled 组件名称(虽然类名看不懂)
  • Chrome DevTools 的 Computed Tab:实时看样式的继承和优先级
  • Emotion 插件支持:VSCode 插件能高亮 emotion 的模板字符串
  • Lighthouse 性能检测:检查是否存在过多嵌套或冗余样式规则

另外,如果你想了解项目中的样式是否真的按需加载,可以试试这个命令:

npx bundlewatch

它会分析你的构建产物,给出各个包的大小分布,帮助你识别潜在问题。


结语:样式方案没有银弹

在这场“CSS-in-JS vs 传统 CSS”的旅程中,我最大的收获不是找到了所谓的“终极答案”,而是明白了技术选型的本质其实是:找到最适合当前团队和项目阶段的最佳平衡点

现在的我们并没有抛弃传统 CSS,也没有全盘拥抱 CSS-in-JS,而是根据场景灵活使用:

  • 基础组件库用 styled-components / emotion
  • 业务页面用 CSS Modules + SCSS 变量管理
  • 全局样式放在单独入口统一注册

如果你正在面临类似的选型难题,不妨先问问自己这几个问题:

  1. 我们团队的协作模式是什么样的?
  2. 是否存在类名冲突或全局污染的历史问题?
  3. 是否需要根据状态动态改变样式?
  4. 项目是否需要 SEO 或 SSR?
  5. 用户体验和首屏性能是我们最关心的吗?

带着这些问题去看文档、跑 Demo,再结合自己的真实需求下判断,远比盲目跟随趋势要靠谱得多。


最后送大家一句话,也是我的一句心得

“样式之争不在于谁更先进,而在于谁能更快让你安心写完组件,然后回家吃饭。”

愿你在每一行代码中都能感受到创造的乐趣 😊

评论 0

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