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

高志华
2025-06-19 19:07
阅读 353

从“类名混乱”到“组件样式即代码”:我在项目中权衡CSS-in-JS和传统CSS的心路历程

从“类名混乱”到“组件样式即代码”:我在项目中权衡CSS-in-JS和传统CSS的心路历程


开篇:为什么这个问题值得聊一聊?

我第一次在React项目中尝试使用CSS-in-JS,是在2019年。那时候我们正在构建一个内部工具平台,用户数量不算太大,但功能模块多、UI组件复杂、迭代频繁。最头疼的是,每次重构样式总是牵一发而动全身——类名冲突、样式污染、维护成本高。那会儿我听说CSS Modules能解决这些问题,后来又看到styled-components如日中天,再之后是emotion、linaria、tailwind等技术轮番登场。

于是,我就陷入了一个困扰每一个前端开发者的问题:“样式应该怎么写?传统的CSS文件还是更现代的CSS-in-JS?”

这五年里,我参与过电商后台系统、数据仪表盘、跨端H5页面等多个项目,每一次都在重新思考这个问题。今天我想结合几个关键项目经历,聊聊我对这个问题的真实思考和选择过程。


问题描述:样式管理为何成了我的噩梦?

记得两年前,我们接了个新项目,目标是开发一个高度可配置的数据看板系统,供多个产品团队复用。项目技术栈基于React + TypeScript,UI组件库自己造了一个轻量级的,以便灵活适配不同产品的视觉风格。

刚开始一切都很顺利,但到了项目中后期,问题逐渐暴露:

  • 类名重复导致样式覆盖(比如container用多了)
  • CSS文件越来越大,难以追踪每个模块到底用了哪些样式
  • 因为组件复用,不同地方引入同一个组件时样式冲突
  • 每次重构样式都需要手动查找HTML结构+修改CSS文件,效率低下
  • 状态驱动样式的逻辑越来越复杂,JS操作DOM属性显得笨重

尤其是当我们要支持主题切换的时候,原本的纯CSS方案几乎没法做动态变量处理。那一刻我意识到:传统的CSS写法已经撑不住项目的复杂度了


解决方案:在CSS-in-JS和传统CSS之间寻找平衡点

我决定换一种思路。既然组件化的思想已经深入前端架构,那么样式能不能也跟着组件化走呢?于是我开始尝试主流的CSS-in-JS方案:

尝试一:styled-components

优点很明显:

  • 样式直接绑定在组件上,阅读性更高
  • 支持props传值驱动样式
  • 主题支持天然友好(ThemeContext)

但在实际使用中我发现:

  • 打包体积偏大(特别是在SSR场景下需要注意注入)
  • 动态样式太多会导致性能瓶颈
  • 调试时class名乱七八糟,想定位元素有点费劲

尝试二:emotion

相比之下,emotion@emotion/react@emotion/styled 性能更好一些,而且对SSR更友好。我们尝试替换了部分核心组件的样式写法,打包体积控制得比styled-components小不少。

尝试三:回归“现代传统CSS” — TailwindCSS

后来我们在另一个小型营销H5项目中尝试了TailwindCSS。说实话,刚接触它的时候我很抗拒:这种写法简直像回到了早期div+table的时代!

但真正用了两周后才发现,它的优势在于:

  • 极致的原子类命名减少了CSS体积
  • 样式写在HTML里,所见即所得
  • 配合PostCSS可以实现主题定制和条件判断
  • Tailwind插件生态丰富,响应式和dark mode都很好用

不过它也有缺点:

  • 项目初期需要统一规范,否则大家写的类名风格不一
  • 学习成本较高(尤其是需要理解其设计哲学)
  • 复杂样式需求(如transition动画)仍然需要自定义CSS配合

代码实践:三种方式的对比示例

假设我要写一个按钮组件,样式根据状态变化而变化(禁用/激活/悬停)。我们看看不同方案怎么写:

✅ 传统CSS写法(配合BEM命名)

/* Button.css */
.btn {
  padding: 0.75rem 1.25rem;
  border-radius: 4px;
  font-weight: 500;
  transition: all 0.2s ease-in-out;
}

.btn--primary {
  background-color: #3b82f6;
  color: white;
}

.btn--primary:hover {
  background-color: #2563eb;
}
import './Button.css';

function Button({ variant = 'primary', disabled, children }) {
  const className = `btn btn--${variant} ${disabled ? 'btn--disabled' : ''}`;
  return <button className={className}>{children}</button>;
}

痛点:随着功能扩展,这个组件可能还会引入更多状态类名,CSS文件也会膨胀;而且容易出现全局污染。


✅ styled-components写法

import styled from 'styled-components';

const StyledButton = styled.button<{ variant: string }>`
  padding: 0.75rem 1.25rem;
  border-radius: 4px;
  font-weight: 500;
  transition: all 0.2s ease-in-out;

  ${({ theme, variant }) =>
    variant === 'primary' &&
    `
    background-color: ${theme.primary};
    color: white;
  `}
`;

function Button({ variant = 'primary', disabled, children }) {
  return (
    <StyledButton variant={variant} disabled={disabled}>
      {children}
    </StyledButton>
  );
}

优点:完全封装样式,主题支持好;缺点是调试时类名不可读,样式加载有延迟。


✅ TailwindCSS写法(配合React)

function Button({ variant = 'primary', disabled, children }) {
  const baseClasses =
    'px-5 py-2 rounded font-medium transition-all duration-200';
  const primaryClasses = disabled
    ? 'bg-gray-400 cursor-not-allowed'
    : 'bg-blue-500 text-white hover:bg-blue-600';

  return (
    <button className={`${baseClasses} ${primaryClasses}`} disabled={disabled}>
      {children}
    </button>
  );
}

特点:写法简洁,但需注意类名顺序影响层叠;适合快速搭建UI。


踩坑经验:真实遇到的那些“坑”

🐞 坑点一:服务端渲染中的样式丢失

在使用styled-components时,如果不正确地进行样式提取,在服务器端渲染的HTML中会出现样式缺失。我们通过引入ServerStyleSheet来解决:

// Next.js App中
import { ServerStyleSheet } from 'styled-components';

export async function getServerSideProps() {
  const sheet = new ServerStyleSheet();
  const html = renderToString(sheet.collectStyles(<App />));
  const styleTags = sheet.getStyleTags();
  return { props: { html, styleTags } };
}

前端性能优化图表-1

不过现在更推荐使用emotion或微调babel插件来优化SSR体验。


⚠️ 坑点二:过度依赖CSS-in-JS的“便利”导致性能瓶颈

在一次性能审计中我们发现,某些页面的首次绘制时间变慢了,原因正是大量使用CSS-in-JS生成的样式对象。后来我们做了两件事:

  1. 对高频组件使用CSS class代替内联样式
  2. 对非动态部分抽离成公共CSS模块

坑点三:Tailwind类名冲突 / 阅读性差

有时候别人接手你的代码,看着一堆m-2 p-3 flex items-center gap-4会觉得很难读懂。我们的解决方案是:

  • 建立命名规范文档
  • 抽离常用类名为函数返回字符串
  • 使用@apply指令合并类名,减少冗余

效果总结:最终选择哪种方案?

我们并没有一味追求某种“终极方案”,而是采用了混合策略:

项目类型 技术选型 理由
中大型SPA应用 emotion + 微型CSS模块 样式随组件走,便于维护
营销页/H5活动页 TailwindCSS + 公共CSS片段 上手快,样式集中可控
内部工具系统 CSS Modules + BEM 控制粒度高,适合长期维护

最终效果:

  • 组件复用率提升 30%
  • 避免样式冲突带来的bug明显减少
  • 新人入职更快熟悉样式结构
  • 页面首屏渲染速度稳定在2秒以内(含第三方脚本)

经验分享:给正在纠结的朋友一些建议

  1. 别迷信任何一种方案,要结合项目生命周期来看。如果你做一个月的活动页面,用Tailwind肯定是最快的;但你要做一个持续一年的产品系统,那就需要更多工程化的考量。

  2. 关注用户体验才是最终目的。无论你写什么技术,都要注意:

    • 首屏是否加载流畅?
    • 交互是否有延迟卡顿?
    • 是否容易引起FOUC(无样式内容闪烁)?
  3. 调试工具很重要。Chrome DevTools的Computed面板、Emmet的类名提示、IDE的样式预览插件都能帮你省很多时间。

  4. 合理拆分样式。即便是CSS-in-JS,也不要把所有东西都写在一个组件里。适当抽离公共样式逻辑,有助于复用与维护。

  5. 保持开放心态。未来CSS的API越来越强大(比如:has()伪类、容器查询),Web Components也开始复兴,也许某一天我们会找到更好的折中方案。


结语:没有完美的答案,只有合适的决策

回头看这几年,其实每种方案都有其适用场景。关键不是“谁更好”,而是“谁更适合当前的业务”。作为前端工程师,我们需要具备根据项目情况做出技术选型的能力,而不是被流行趋势牵着鼻子走。

最后送一句话给我自己,也送给你们:“写得一手漂亮的样式很重要,但更重要的是写出能在时间长河中依然稳健运行的代码。”

如果你也在做类似的技术选型,欢迎留言交流一下你的项目背景,我可以一起帮你分析最适合的方案 💬

评论 0

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