CSS-in-JS vs 传统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 } };
}

不过现在更推荐使用emotion或微调babel插件来优化SSR体验。
⚠️ 坑点二:过度依赖CSS-in-JS的“便利”导致性能瓶颈
在一次性能审计中我们发现,某些页面的首次绘制时间变慢了,原因正是大量使用CSS-in-JS生成的样式对象。后来我们做了两件事:
- 对高频组件使用CSS class代替内联样式
- 对非动态部分抽离成公共CSS模块
❗ 坑点三:Tailwind类名冲突 / 阅读性差
有时候别人接手你的代码,看着一堆m-2 p-3 flex items-center gap-4会觉得很难读懂。我们的解决方案是:
- 建立命名规范文档
- 抽离常用类名为函数返回字符串
- 使用@apply指令合并类名,减少冗余
效果总结:最终选择哪种方案?
我们并没有一味追求某种“终极方案”,而是采用了混合策略:
| 项目类型 | 技术选型 | 理由 |
|---|---|---|
| 中大型SPA应用 | emotion + 微型CSS模块 | 样式随组件走,便于维护 |
| 营销页/H5活动页 | TailwindCSS + 公共CSS片段 | 上手快,样式集中可控 |
| 内部工具系统 | CSS Modules + BEM | 控制粒度高,适合长期维护 |
最终效果:
- 组件复用率提升 30%
- 避免样式冲突带来的bug明显减少
- 新人入职更快熟悉样式结构
- 页面首屏渲染速度稳定在2秒以内(含第三方脚本)
经验分享:给正在纠结的朋友一些建议
别迷信任何一种方案,要结合项目生命周期来看。如果你做一个月的活动页面,用Tailwind肯定是最快的;但你要做一个持续一年的产品系统,那就需要更多工程化的考量。
关注用户体验才是最终目的。无论你写什么技术,都要注意:
- 首屏是否加载流畅?
- 交互是否有延迟卡顿?
- 是否容易引起FOUC(无样式内容闪烁)?
调试工具很重要。Chrome DevTools的Computed面板、Emmet的类名提示、IDE的样式预览插件都能帮你省很多时间。
合理拆分样式。即便是CSS-in-JS,也不要把所有东西都写在一个组件里。适当抽离公共样式逻辑,有助于复用与维护。
保持开放心态。未来CSS的API越来越强大(比如
:has()伪类、容器查询),Web Components也开始复兴,也许某一天我们会找到更好的折中方案。
结语:没有完美的答案,只有合适的决策
回头看这几年,其实每种方案都有其适用场景。关键不是“谁更好”,而是“谁更适合当前的业务”。作为前端工程师,我们需要具备根据项目情况做出技术选型的能力,而不是被流行趋势牵着鼻子走。
最后送一句话给我自己,也送给你们:“写得一手漂亮的样式很重要,但更重要的是写出能在时间长河中依然稳健运行的代码。”
如果你也在做类似的技术选型,欢迎留言交流一下你的项目背景,我可以一起帮你分析最适合的方案 💬

评论 0