CSS-in-JS 还是传统 CSS?从实战中总结的现代样式方案选型指南
去年我带着团队重构公司核心产品的时候,第一个摆在面前的难题就是:我们该用传统 CSS,还是转向 CSS-in-JS?
这个问题看似简单,实则背后牵扯到项目结构、组件复用性、开发效率、协作方式等多个层面。作为技术负责人,我知道做出错误选择可能直接影响整个项目的推进节奏和维护成本。于是我们花了整整两周时间做了技术调研与对比验证,最终根据项目特点选择了合适的方案。
现在回过头来看当时的决策过程,我想把这段经历写出来,希望能给同样面临选择的你带来一些启发。
背景介绍:为什么我们要重新审视样式管理?


我们的项目是一个面向企业用户的后台管理系统,使用 React + TypeScript 构建,已有 3 年历史。随着功能不断叠加,CSS 风格混乱、类名冲突、样式难以维护的问题日益严重。比如,某个老同事写的 .title 类,在多个地方被覆盖,导致页面标题时而加粗、时而变色。
我们意识到,这种“全局污染”的问题如果不解决,未来的可维护性将越来越差。当时团队内部对是否继续使用原始 CSS 模块化方案产生分歧:有人担心 CSS Modules 写起来不够灵活;也有人反对引入第三方 CSS-in-JS 库,担心学习成本和打包体积。
于是我们决定做一次全面的技术评估,并在 Demo 项目中实际体验两种方案的表现。
实战中的挑战:传统 CSS 的局限性暴露无遗

我们在原有系统的基础上建立了一个新模块的实验分支,目标是构建一个支持动态换肤的数据看板组件,同时需要应对如下几个关键场景:
- 样式嵌套深:数据卡片内部有多层结构,依赖嵌套类名来定义样式;
- 主题切换需求强:不同用户希望看到不同的主色调、字体大小等;
- 样式高度定制化:每个卡片可以自定义边框、背景、阴影等样式;
- 多人协同开发:前后端同学都可能会参与组件样式修改。
当我们试图用传统的 CSS 方案(搭配 CSS Modules)来实现时,问题接连出现:
- 类名命名难:为了防止重复和污染,
.card__item--active这种命名方式虽然有效,但在多层嵌套中极难维护。 - 样式作用域控制不彻底:某些动画或浮动框样式依然会影响到其他区域,尤其是在使用第三方组件库时。
- 主题变量传递不便:尽管用了 CSS 变量,但当主题需要运行时动态改变时,处理起来异常麻烦。
更头疼的是,有一天后端小哥帮忙改了个类名拼写错误,结果整张卡牌的布局全乱了,这说明即使有了 CSS Modules,全局状态依然存在隐式耦合的风险。
技术探索:尝试 CSS-in-JS 的可行性

为了找到更好的解决方案,我们引入了 styled-components 做试点改造。刚开始时有几个疑问:
- 使用字符串模板写样式会不会很反直觉?
- 组件多了以后性能会不会下降?
- 是否会影响 SSR 或 SEO?
- 主题动态切换能不能真正落地?
先说结论:这些问题都有解,只是需要合理使用工具和方法。
举个例子,在数据看板的组件中,我们可以这样写:
// CardContainer.tsx
import styled from 'styled-components';
interface CardProps {
themeColor: string;
}
const CardContainer = styled.div<CardProps>`
background-color: #fff;
border: 1px solid ${props => props.themeColor};
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
`;
export default function DataCard({ themeColor }: { themeColor: string }) {
return (
<CardContainer themeColor={themeColor}>
{/* 卡片内容 */}
</CardContainer>
);
}
通过这种方式,我们不仅实现了样式和组件的逻辑绑定,还能非常方便地注入动态值,比如颜色、padding 大小、字体尺寸等。
改造过程中踩过的坑与经验分享


🐞 1. 样式标签未隔离导致的 SSR 闪屏
我们一开始没有配置主题上下文服务端渲染(Next.js 环境),导致首屏加载时默认样式短暂显示后又被替换,造成样式闪烁(FOUC)。后来我们统一使用 ThemeProvider 包裹根组件,并确保主题对象在服务端可用,才解决了这个问题。
// _app.tsx (Next.js)
import { ThemeProvider } from 'styled-components';
import theme from '../themes/default';
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
);
}
💥 2. 开发者工具调试困难
最开始有些前端同学反映,浏览器 DevTools 中看不到具体的 class 名,只看到一串随机生成的哈希,这对调试十分不利。
后来我们发现可以通过设置 displayName 和 pureComponent 来让样式更加清晰可读,甚至配合 ESLint 插件来统一命名规则。
⏱️ 3. 性能问题
我们在测试环境中模拟加载上千个组件的情况,确实遇到了一些性能瓶颈。但这是极端场景下的表现,实际使用中并不会达到这个数量级。为了预防潜在问题,我们采取了如下措施:
- 避免在 render 函数中频繁创建新的样式的组件
- 优先使用组件组合而非内联条件表达式生成样式
- 对于基础样式,保留少量 CSS 模块文件供引用
这些优化手段有效提升了渲染性能,同时也增强了代码的可读性。
最终效果与收益对比
我们将新旧方案分别应用到两个相似的功能模块中进行对照实验,结果如下:
| 维度 | 传统 CSS(CSS Modules) | CSS-in-JS(styled-components) |
|---|---|---|
| 开发效率 | ✅ 类名复杂,易出错 | ✅ 直接绑定属性,逻辑清晰 |
| 维护难度 | ❌ 全局污染隐患明显 | ✅ 作用域封闭,不易交叉影响 |
| 主题动态切换 | ❌ 需手动更新 CSS 变量 | ✅ 内置支持主题透传机制 |
| 打包体积 | ✅ 更小 | ❌ 引入 runtime 成本略高 |
| 学习成本 | ⚠️ 固定模式,需熟悉命名规范 | ⚠️ 新语法需适应 |
整体来看,CSS-in-JS 在组件级封装、可维护性方面表现出显著优势。尽管打包体积略有增加,但对于大多数中小型项目来说是可以接受的。
给开发者的几点建议
如果你正在面临类似的抉择,以下是我结合真实项目经验和行业趋势的一些个人看法:
- 不要盲目追随潮流:CSS-in-JS 并非银弹。如果你的项目偏静态、UI 变化不大,传统 CSS 模块仍然是性价比很高的选择。
- 按场景选择合适工具:
- 如果是基于 React 的大型组件库/设计系统,推荐使用
styled-components或emotion - 如果追求极致性能,可以尝试 Tailwind CSS + CSS Module 混合方案
- 如果使用 Vue / Svelte,也有对应的解决方案如
Vue-styled-components
- 如果是基于 React 的大型组件库/设计系统,推荐使用
- 注意主题一致性管理:无论是哪一种方案,都应有一套统一的设计语言系统(Design System),避免陷入“各自为政”的困境。
- 善用开发工具:
- 使用 Styleguidist 或 Storybook 快速构建组件文档
- 结合 Chrome DevTools 的 computed tab 查看样式注入情况
- 拥抱渐进迁移策略:不必一次性全部切换,可以在新组件中逐步采用 CSS-in-JS,旧组件维持原样,降低风险。
小结
回想起那次重构前的纠结,我深刻体会到选型没有绝对的对与错,只有是否适合当前项目的特点和团队现状。
我们最终在新模块中采用 CSS-in-JS,并在后续版本中逐步推广至核心组件,收到了良好的反馈。它帮助我们大幅提升了组件封装能力和维护效率,也让我们能更从容地面对未来可能的 UI 重构需求。
如果你也在思考如何管理现代 Web 应用的样式,不妨动手尝试下 CSS-in-JS,亲自感受一下它的灵活性与力量。毕竟,真正的答案不在理论中,而在你的项目代码里。
“工欲善其事,必先利其器。” ——《论语》

评论 0