我踩了三年坑,终于搞懂了CSS-in-JS和传统CSS该怎么选
写在前面的话:最近B站评论区好多粉丝私信问我"到底该用传统CSS还是CSS-in-JS",我当初刚入行的时候也被这个问题折磨得够呛,今天就把我踩过的坑一次性讲清楚。
一、先搞清楚这两货到底是啥
1.1 传统CSS:老派但经典
传统CSS就是我们最熟悉的写法,把样式单独写在一个.css文件里,然后通过类名、ID等方式应用到HTML元素上。
/* style.css */
.button {
background-color: #1890ff;
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.button:hover {
background-color: #40a9ff;
}
<!-- index.html -->
<button class="button">点击我</button>
我当初学的时候,觉得这种写法简直完美——样式和结构分离,各司其职,多清晰啊!直到我接手了一个有200多个CSS文件的老项目……
1.2 CSS-in-JS:把样式搬进JavaScript
CSS-in-JS的核心思想是:直接在JavaScript代码中写CSS样式,样式和组件绑定在一起。
// Button.jsx
import styled from 'styled-components';
const Button = styled.button`
background-color: #1890ff;
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
&:hover {
background-color: #40a9ff;
}
`;
export default function App() {
return <Button>点击我</Button>;
}
看到区别了吗?样式直接"住"在组件里,不用再去CSS文件里找对应的类名了。
二、环境准备:手把手带你搭起来
2.1 传统CSS方案环境搭建
用Vite创建一个最基础的React项目:
npm create vite@latest css-traditional-demo -- --template react
cd css-traditional-demo
npm install
npm run dev
项目结构如下:
css-traditional-demo/
├── src/
│ ├── App.jsx
│ ├── App.css # 组件级样式
│ ├── index.css # 全局样式
│ └── main.jsx
├── index.html
└── package.json
2.2 CSS-in-JS方案环境搭建
同样用Vite创建项目,然后安装styled-components:
npm create vite@latest css-in-js-demo -- --template react
cd css-in-js-demo
npm install
npm install styled-components
npm run dev
如果你用的是TypeScript,还需要安装类型定义:
npm install -D @types/styled-components
三、核心概念对比:一张表讲清楚
| 对比维度 | 传统CSS | CSS-in-JS(styled-components) |
|---|---|---|
| 样式位置 | 独立的.css文件 | 写在JS/JSX组件内部 |
| 类名生成 | 手动命名,容易冲突 | 自动生成唯一类名,零冲突 |
| 动态样式 | 需要拼接类名或内联style | 直接通过props控制,非常自然 |
| 代码拆分 | 样式和组件分离,需来回切换 | 样式和组件在一起,所见即所得 |
| 死代码清理 | 手动排查,容易遗漏 | 删除组件=删除样式,自动清理 |
| 学习成本 | 低,入门简单 | 中等,需要理解JSX和模板字符串 |
| 运行时开销 | 无 | 有一定运行时开销 |
| 服务端渲染 | 天然支持 | 需要额外配置 |
| 浏览器调试 | 直接看CSS文件 | 看到的是生成的哈希类名 |
四、实战对比:做一个卡片组件
光说概念太抽象了,咱们直接上手写一个实际组件,两种方案都实现一遍,你自然就能感受到差异。
4.1 需求描述
做一个用户信息卡片,包含:
- 头像(圆形)
- 用户名
- 个人简介
- 支持"高亮"和"普通"两种主题
4.2 传统CSS实现
/* Card.css */
.card {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
max-width: 360px;
display: flex;
align-items: flex-start;
gap: 16px;
transition: box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card--highlighted {
border-color: #1890ff;
background-color: #f0f7ff;
}
.card__avatar {
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.card__content {
flex: 1;
min-width: 0;
}
.card__name {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
color: #333;
}
.card__bio {
font-size: 14px;
color: #666;
margin: 0;
line-height: 1.6;
}
// Card.jsx
import './Card.css';
export default function Card({ name, bio, avatar, highlighted = false }) {
const cardClassName = highlighted ? 'card card--highlighted' : 'card';
return (
<div className={cardClassName}>
<img className="card__avatar" src={avatar} alt={name} />
<div className="card__content">
<h3 className="card__name">{name}</h3>
<p className="card__bio">{bio}</p>
</div>
</div>
);
}
// App.jsx
import Card from './Card';
export default function App() {
return (
<div style={{ padding: 40, display: 'flex', gap: 24 }}>
<Card
name="张三"
bio="前端开发工程师,热爱开源"
avatar="https://i.pravatar.cc/64?u=zhangsan"
/>
<Card
name="李四"
bio="全栈工程师,专注于React生态"
avatar="https://i.pravatar.cc/64?u=lisi"
highlighted={true}
/>
</div>
);
}
4.3 CSS-in-JS实现
// Card.jsx
import styled, { css } from 'styled-components';
const CardWrapper = styled.div`
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
max-width: 360px;
display: flex;
align-items: flex-start;
gap: 16px;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
${props => props.$highlighted && css`
border-color: #1890ff;
background-color: #f0f7ff;
`}
`;
const Avatar = styled.img`
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
`;
const Content = styled.div`
flex: 1;
min-width: 0;
`;
const Name = styled.h3`
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
color: #333;
`;
const Bio = styled.p`
font-size: 14px;
color: #666;
margin: 0;
line-height: 1.6;
`;
export default function Card({ name, bio, avatar, highlighted = false }) {
return (
<CardWrapper $highlighted={highlighted}>
<Avatar src={avatar} alt={name} />
<Content>
<Name>{name}</Name>
<Bio>{bio}</Bio>
</Content>
</CardWrapper>
);
}
4.4 对比分析
看完两段代码,你会发现几个关键差异:
传统CSS的痛点:
- 动态样式靠拼接类名
card card--highlighted,类名多了容易出错 - 需要同时维护
.jsx和.css两个文件,来回切换 - BEM命名规范(
card__name)虽然好用,但写起来确实啰嗦
CSS-in-JS的优势:
highlighted属性直接通过props传递,逻辑清晰- 所有样式和组件在同一个文件里,不用跳来跳去
- 组件删除时样式自动清理,不会有残留的死代码
五、进阶场景:动态主题切换
实际项目中经常需要支持主题切换,咱们看看两种方案分别怎么处理。
5.1 传统CSS方案:CSS变量 + 类名切换
/* theme.css */
:root {
--bg-primary: #ffffff;
--text-primary: #333333;
--accent-color: #1890ff;
}
:root[data-theme="dark"] {
--bg-primary: #1a1a2e;
--text-primary: #e0e0e0;
--accent-color: #4fc3f7;
}
.app {
background-color: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
transition: background-color 0.3s ease;
}
.button {
background-color: var(--accent-color);
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
// App.jsx
import { useState } from 'react';
import './theme.css';
export default function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
};
return (
<div className="app">
<button className="button" onClick={toggleTheme}>
切换到{theme === 'light' ? '暗色' : '亮色'}模式
</button>
</div>
);
}
5.2 CSS-in-JS方案:ThemeProvider
// theme.js
export const lightTheme = {
bgPrimary: '#ffffff',
textPrimary: '#333333',
accentColor: '#1890ff',
};
export const darkTheme = {
bgPrimary: '#1a1a2e',
textPrimary: '#e0e0e0',
accentColor: '#4fc3f7',
};
// App.jsx
import { useState } from 'react';
import styled, { ThemeProvider } from 'styled-components';
import { lightTheme, darkTheme } from './theme';
const AppWrapper = styled.div`
background-color: ${props => props.theme.bgPrimary};
color: ${props => props.theme.textPrimary};
min-height: 100vh;
padding: 20px;
transition: background-color 0.3s ease;
`;
const Button = styled.button`
background-color: ${props => props.theme.accentColor};
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
`;
export default function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const currentTheme = theme === 'light' ? lightTheme : darkTheme;
return (
<ThemeProvider theme={currentTheme}>
<AppWrapper>
<Button onClick={toggleTheme}>
切换到{theme === 'light' ? '暗色' : '亮色'}模式
</Button>
</AppWrapper>
</ThemeProvider>
);
}
CSS-in-JS的主题方案明显更优雅——ThemeProvider把主题对象注入到整个组件树,子组件通过props.theme直接访问,类型安全且逻辑集中。
六、我踩过的坑(血泪经验)
坑1:styled-components的性能陷阱
// ❌ 错误写法:每次渲染都创建新的styled组件
export default function List({ items }) {
return items.map(item => {
const ItemWrapper = styled.div` // 这行代码每次渲染都会执行!
padding: 10px;
border-bottom: 1px solid #eee;
`;
return <ItemWrapper key={item.id}>{item.name}</ItemWrapper>;
});
}
// ✅ 正确写法:把styled组件定义在组件外部
const ItemWrapper = styled.div`
padding: 10px;
border-bottom: 1px solid #eee;
`;
export default function List({ items }) {
return items.map(item => (
<ItemWrapper key={item.id}>{item.name}</ItemWrapper>
));
}
我当初学的时候犯了这个错误,列表一长页面直接卡死。记住:styled.xxx要在组件外部定义,它本质上是在创建一个新的React组件。
坑2:传统CSS的全局污染问题
/* component-a.css */
.title {
font-size: 24px;
color: red;
}
/* component-b.css */
.title {
font-size: 16px;
color: blue;
}
两个组件都用了.title这个类名,后加载的样式会覆盖先加载的。解决方案有几种:
- 使用BEM命名规范手动避免冲突
- 使用CSS Modules(
.module.css后缀) - 使用CSS-in-JS自动生成唯一类名
坑3:CSS-in-JS的服务端渲染问题
如果你用Next.js + styled-components,必须配置babel插件,否则服务端渲染时样式会闪烁:
// .babelrc 或 next.config.js
{
"presets": ["next/babel"],
"plugins": [["styled-components", { "ssr": true }]]
}
坑4:调试CSS-in-JS时的类名困惑
CSS-in-JS生成的类名长这样:sc-bdVaJa fGQjPz,在浏览器DevTools里看到这种类名会一脸懵。
解决方法——开启displayName:
// babel配置
{
"plugins": [["styled-components", { "displayName": true }]]
}
开启后类名会变成:Button__StyledButton-sc-1a2b3c,调试体验好很多。
七、到底该怎么选?我的建议
| 项目类型 | 推荐方案 | 理由 |
|---|---|---|
| 小型项目/个人项目 | 传统CSS或CSS Modules | 简单直接,没有额外依赖 |
| 中大型组件库 | CSS-in-JS | 主题系统、动态样式、样式隔离都很方便 |
| 内容型网站(博客、官网) | 传统CSS | 性能优先,SEO友好,无运行时开销 |
| 后台管理系统 | CSS Modules 或 CSS-in-JS | 组件多、复用多,需要样式隔离 |
| 需要动态主题切换 | CSS-in-JS | ThemeProvider方案最优雅 |
| 性能敏感场景 | 传统CSS | 零运行时开销,首屏加载更快 |
说句掏心窝的话: 没有绝对的好坏,只有适不适合。我见过用CSS-in-JS做简单官网结果包体积暴增的,也见过用传统CSS做大型组件库结果样式冲突改到崩溃的。
八、新手常见问题FAQ
Q1:我是纯新手,应该先学哪个?
先学传统CSS。把选择器、盒模型、Flexbox、Grid这些基础打牢,再去学CSS-in-JS。否则你连CSS属性都不认识,用CSS-in-JS也只是换了个地方写你不懂的东西。
Q2:CSS-in-JS会不会被淘汰?
目前来看不会。虽然"零运行时"方案(如vanilla-extract、Linaria)在崛起,但CSS-in-JS的核心思想——样式与组件共存——已经被广泛接受。
Q3:能不能两种方案混用?
可以,但不推荐。混用会增加心智负担,团队协作时也容易产生分歧。选定一个方案就坚持用下去。
Q4:CSS Modules算CSS-in-JS吗?
不算。CSS Modules本质上还是传统CSS文件,只是通过构建工具自动生成唯一类名来实现样式隔离。它没有运行时开销,是一个很好的折中方案。
/* Button.module.css */
.button {
background-color: #1890ff;
color: white;
padding: 8px 16px;
border-radius: 4px;
}
// Button.jsx
import styles from './Button.module.css';
export default function Button({ children }) {
return <button className={styles.button}>{children}</button>;
}
九、下一步学习建议
按照这个路径来,稳扎稳打:
第1周:传统CSS基础
├── 选择器与优先级
├── 盒模型与布局(Flexbox/Grid)
└── 响应式设计(媒体查询)
第2周:CSS工程化
├── CSS Modules
├── CSS预处理器(Sass/Less)
└── PostCSS与Autoprefixer
第3周:CSS-in-JS入门
├── styled-components基础用法
├── 动态样式与props
└── ThemeProvider主题系统
第4周:实战与对比
├── 用两种方案实现同一个项目
├── 性能对比测试
└── 根据场景做技术选型
十、写在最后
我写这篇教程的初衷,就是看到太多新手在"传统CSS还是CSS-in-JS"这个问题上纠结太久。说实话,这个问题没有标准答案,关键是理解每种方案的优缺点,然后根据你的项目需求做选择。
我当初花了大半年时间,在三个不同项目里分别用了传统CSS、CSS Modules和styled-components,才真正理解了它们各自适合什么场景。所以我的建议是——别光看文章,动手写起来,踩过的坑才是你真正的经验。
如果你在学习过程中遇到问题,欢迎来B站找我,评论区见!我们下期教程再见。


评论 0