从传统CSS到CSS-in-JS:我如何在大型项目中做出样式方案的抉择
开篇 | 一场重构带来的思考

去年我在参与公司核心产品的前端重构时,遇到了一个很典型的问题:随着项目规模的增长,传统的CSS管理模式已经越来越难维护了。组件复用性低、命名冲突严重、样式难以追踪,尤其是在不同团队协同开发时,样式污染和样式优先级问题经常让我们焦头烂额。
我们当时面临两个选择:
- 继续使用传统的 CSS 模块化方案(BEM + SCSS)
- 尝试迁移到 CSS-in-JS 的现代方案(我们选的是 styled-components)
最终我们选择了后者,并且这个决定确实带来了很多好处——但也踩了不少坑。今天我就想结合这次真实的项目经验,来聊一聊“CSS-in-JS vs 传统CSS”这个话题,不讲太多理论,直接说干活内容。
问题描述 | 当传统CSS开始变得捉襟见肘


我们的项目是一个中后台系统,基于 React 开发,功能模块多,组件库庞大,团队协作频繁。原本采用的是 SCSS + BEM 的方式组织样式,初期一切良好,但到了后期维护阶段,问题接踵而至:
- 类名冲突频繁出现:比如 .btn、.card 这样的通用样式,在多个页面/组件中被重复定义,容易互相覆盖。
- 样式作用域不好控制:即便用了 BEM 命名规范,也无法完全避免全局污染,尤其是第三方组件混入进来时更糟。
- 主题定制困难:整个系统的主题变量散落在多个地方,修改颜色、字号等配置需要改多个文件。
- 调试困难:Chrome DevTools 查看元素样式时,往往无法直观看到对应代码位置,特别是样式继承层级复杂的时候。
- 组件封装不彻底:虽然组件本身是封装好的,但样式仍需外部引入 SCSS 文件,增加了耦合度。
这些问题让新入职的同学上手特别慢,也让我们自己每次提测都要花大量时间查样式 bug。
解决方案 | 是时候尝试 CSS-in-JS 了
为了解决上面这些问题,我们在一次架构讨论会上提出了两个初步方案:
- 继续优化现有结构:升级 SCSS 工具链,引入 CSS Modules 和 PostCSS 插件提升模块化能力
- 尝试 CSS-in-JS 方案:主流有 styled-components、emotion、linaria 等,其中我们对 styled-components 更感兴趣
最终我们做了个小对比实验,把某个高频复用的组件分别用两种方式实现,然后拉上产品和 QA 同学一起评审。结果表明:
| 维度 | 传统SCSS | CSS-in-JS(styled-components) |
|---|---|---|
| 开发效率 | 中等 | 提升明显 |
| 调试体验 | 一般 | 更好(可定位具体组件) |
| 样式隔离 | 靠自觉 | 自动隔离 |
| 主题支持 | 配置分散 | 内建 theme provider |
| 构建速度 | 快 | 略慢(但影响不大) |
| 新人上手 | 困难 | 易于理解 |

综合下来我们决定:在新模块中启用 styled-components,老模块逐步迁移。这是一个折中但务实的选择,不会一开始就全部重写,风险可控。
实践落地 | 我们是如何推进的
项目背景简要说明
我们项目整体架构如下:
- 技术栈:React 17 + TypeScript + Webpack + ESLint + Styled-Components
- UI框架:Ant Design(部分自研组件)
- 项目类型:PC 端管理系统,用户量大,交互较多
目标是在新增需求模块中优先使用 styled-components,同时提供工具辅助旧模块迁移。
技术方案设计
我们采用了一个渐进式过渡策略:
第一阶段:新建模块统一使用 styled-components
- 定义新的组件样式文件以
.styled.ts结尾,如Button.styled.ts - 每个组件只引入自身样式文件,实现真正的“组件即样式容器”
- 使用
ThemeProvider实现主题配置共享
示例代码:
// Button.styled.ts
import styled from 'styled-components';
export const StyledButton = styled.button`
padding: 8px 16px;
border-radius: 4px;
background-color: ${props => props.theme.primaryColor};
color: #fff;
border: none;
cursor: pointer;
&:hover {
opacity: 0.9;
}
`;
// 在组件中使用
import { StyledButton } from './Button.styled';
const Button = () => (
<StyledButton>点击我</StyledButton>
);
第二阶段:为老组件提供迁移指南与脚本
我们开发了一个简单的代码转换工具,帮助将 SCSS 转换成对应的 styled-components 格式,虽然不能完全自动化,但能大大节省人工工作量。
例如自动将:
.my-btn {
padding: 8px 16px;
color: white;
}
转成:
const MyBtn = styled.div`
padding: 8px 16px;
color: white;
`;
第三阶段:建立统一的主题管理机制
通过 theme-provider 统一管理所有样式变量,比如主色、字号、间距等,确保视觉一致性。
// theme.ts
export default {
primaryColor: '#1890ff',
fontSize: '14px',
};
// App.tsx
import { ThemeProvider } from 'styled-components';
import theme from './theme';
function App() {
return (
<ThemeProvider theme={theme}>
<Router>
<Routes />
</Router>
</ThemeProvider>
);
}
这样在任何组件中都可以轻松访问主题属性:
background-color: ${props => props.theme.primaryColor};
踩过的坑 & 小插曲
再好的方案也会遇到问题,下面是我们在实际推进过程中遇到的一些挑战及解决办法:
1. 类型提示缺失 → 引入 TypeScript 支持
一开始我们只是用了 styled-components 的基本用法,没做类型绑定,导致 IDE 无法自动提示 theme 中的字段,容易出错。
解决方案:
安装 @types/styled-components 并定义接口:
import 'styled-components';
declare module 'styled-components' {
export interface DefaultTheme {
primaryColor: string;
fontSize: string;
}
}
这之后,敲 props.theme. 就能获得完整的自动补全了。
2. SSR 不友好 → 增加服务器端渲染适配
我们在服务端渲染项目中使用 Next.js 时发现,首次加载没有注入 CSS,导致闪烁。
解决方法:
引入 ServerStyleSheet 并在 _document.tsx 中处理:
// pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
render() {
return (
<Html lang="zh-CN">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
3. 动态样式逻辑复杂时维护成本高
有时候我们会写类似这样的条件判断:
${props => props.isActive && css`color: red;`}
但当逻辑变多以后,会变得很难读。
后来我们把一些复杂的逻辑抽象成函数形式:
const getButtonStyle = (props) => `
background-color: ${props.theme.primaryColor};
color: #fff;
`;
并配合 as const 来减少重复字符串模板拼接。
4. 编译性能略微下降
在使用 styled-components 时,构建时间会比纯 CSS 略长,特别是在热更新时。我们尝试过 linaria、emotion,最终因为团队成员熟悉度选择了 styled-components。
如果对性能特别敏感,可以考虑像 linaria 这样提前编译成静态 CSS 的方案,更适合 SSR 或对首屏性能要求高的项目。
效果总结 | 成效显著,收益颇多
经过一年的实际使用,我们得出了几点明显的改进:
- 组件样式内聚性强:每个组件都自带样式声明,更容易理解和复用
- 样式污染大幅减少:每个组件都有唯一的类名生成,不会有意外覆盖
- 调试更方便:DevTools 可清晰看到组件对应样式,甚至可以直接跳转到源码
- 主题管理更加灵活:修改主题不再需要翻找一堆变量文件,集中一处即可生效
- 新人上手成本降低:代码结构清晰,样式逻辑集中在一个文件里,学习曲线平缓
不过也有一些小遗憾,比如打包体积略涨、初次加载样式延迟一点点,但这些都在可接受范围内。
个人建议 | 到底怎么选?
如果你也在纠结要不要从传统 CSS 迁移到 CSS-in-JS,以下是我的几点真实建议:
✅ 推荐使用 CSS-in-JS 的场景
- 组件驱动开发的 React/Vue 项目
- 多人协作、组件复用率高、样式复杂
- 需要主题化、动态样式配置
- 对开发效率和调试体验有较高要求
推荐方案:
- styled-components(社区成熟,文档丰富)
- emotion(兼顾性能和灵活性)
❌ 不太适合的场景
- SEO/首屏优化极其严苛的项目(比如门户首页)
- 已有大量 CSS 积累,且无计划重构的项目
- 技术栈偏保守(公司文化或团队风格所致)
总结 | 不是银弹,但确实有用
CSS-in-JS 并不是万能的“银弹”,但它的确解决了我们在工程化过程中碰到的真实痛点。它不是为了炫技或者追潮流,而是为了提升我们日常工作的效率、降低维护成本。
最后分享一句我常跟团队说的话:
“技术方案从来都不是非黑即白,关键是它能不能解决你当前的问题。”
希望这篇文章能帮你少走点弯路,在面对样式方案选择时更有底气地做出决策。如果你还在坚持传统 CSS,那也没关系——重要的是写出清晰、可维护、易扩展的代码,无论用什么手段,达成目标才是关键。
💡 小贴士:如果你刚开始接触 styled-components,不妨试试 VSCode 的 styled-components 插件(如 “vscode-styled-components”),它不仅能语法高亮,还支持自动补全和样式预览哦!
如有疑问欢迎留言交流,我也曾在这个坑里摸爬滚打很久,很乐意一起探讨 👍

评论 0