我在通勤地铁上想通的CSS方案选型这件事
先说结论:没有银弹。做中大型前端项目、团队混合React和Vue时,我的倾向是——用CSS Modules做基础方案,局部复杂组件用CSS-in-JS兜底。
我是怎么被这个问题折磨的
去年双11前,我们接了个紧急营销活动页需求,技术栈是React 18 + Webpack 5,样式用传统全局CSS加BEM命名。结果倒计时组件和商品列表组件里各有一个.countdown-item,两个样式文件被引入同一页面后互相覆盖,数字变绿、背景图裂了。那天晚上十一点我还在跟QA对线。
这个事让我认真思考:传统CSS的全局作用域问题到底怎么解?
三条路我都试过
传统CSS+BEM:老牌方案,但真累
BEM用命名规范模拟作用域,类名写起来手酸,而且完全靠自觉。优点是浏览器原生支持,性能最好,DevTools调试直观。但团队协作时规范靠不住,赶deadline时没人能保证严格遵守BEM。
CSS Modules:最平衡的方案
还是写普通.css文件,但构建工具会把类名编译成唯一哈希,从根源上解决全局污染,同时保留CSS全部优点:原生解析、可调试、零运行时开销。
/* Countdown.module.css */
.countdown-item {
display: flex;
align-items: center;
font-size: 24px;
color: #ff4d4f;
}
import styles from './Countdown.module.css';
function Countdown() {
return (
<div className={styles['countdown-item']}>
<span>23</span>:<span>59</span>:<span>59</span>
</div>
);
}
缺点是动态样式不好处理,比如根据用户等级变主题色或动态计算top值,只能退回内联样式。
CSS-in-JS:灵活是真的灵活,重也是真的重
代表是styled-components和Emotion,用JS能力生成样式:
import styled from 'styled-components';
const CountdownItem = styled.span`
display: flex;
align-items: center;
font-size: 24px;
color: ${props => props.isUrgent ? '#ff4d4f' : '#333'};
`;
动态样式非常自然,样式随组件删除不产生“孤儿CSS”。但问题明显:运行时开销——我们项目首屏从1.8s涨到2.4s;调试体验差——DevTools里全是sc-bdVaJa这类哈希类名;SSR复杂度高——样式注入时机不对会导致首屏闪烁(FOUC)。
一个真实的性能对比
商品列表页实测数据:
| 方案 | 首屏渲染时间 | JS Bundle体积 | 样式加载方式 | 开发体验 |
|---|---|---|---|---|
| 传统CSS+BEM | 1.8s | 0KB额外开销 | 原生CSS文件 | 一般,需手动管理命名 |
| CSS Modules | 1.85s | 几乎可忽略 | 编译后的CSS文件 | 好,有类型提示 |
| styled-components | 2.4s | 约45KB(gzip后15KB) | JS运行时注入 | 好,动态样式方便 |
趋势明确:CSS-in-JS运行时方案在性能上确实有代价。zero-runtime方案如Linaria、vanilla-extract虽无运行时开销,但学习成本和配置复杂度更高。
我现在的选择:分层策略
第一层:全局样式用传统CSS。 reset、字体、颜色变量、通用工具类(.flex-center、.text-ellipsis),加载快、维护简单。
第二层:组件样式用CSS Modules。 90%以上的业务组件都这么写,隔离作用域且无运行时开销。
第三层:动态样式需求用CSS-in-JS。 只有需要根据props动态计算样式的组件才用,如主题切换、动态布局、用户自定义样式。
这套策略跑了大半年,首屏保持在1.9s左右,开发效率没有明显下降。
一些踩坑记录
坑一:CSS Modules和全局样式的混用。 :global包裹的样式会让依赖关系不透明,尽量少用,必须用时在注释里写清楚依赖。
坑二:CSS-in-JS的SSR样式闪烁。 用Next.js必须配置_document.js:
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();
}
}
}
漏了sheet.seal()会内存泄漏。
坑三:CSS-in-JS的类型提示。 TypeScript下需在styled.d.ts声明全局主题类型:
import 'styled-components';
declare module 'styled-components' {
export interface DefaultTheme {
colors: {
primary: string;
danger: string;
};
spacing: {
small: string;
medium: string;
};
}
}
这个配置经常被忽略,导致新同事写主题样式没有类型提示。
一些不成熟的想法
CSS方案的演进有点像C++的发展史:传统CSS像早期C,简单直接但容易出错;CSS Modules像命名空间,解决命名冲突;CSS-in-JS像模板元编程,强大但复杂,用不好反而伤身。核心逻辑是通的:工具的选择应该服务于你的场景,而不是为了用工具而用工具。
最后说两句
回到开头的问题:CSS-in-JS vs 传统CSS怎么选?看场景。小型项目或原型验证怎么快怎么来;中大型项目建议CSS Modules为主、CSS-in-JS为辅;对性能有极致要求,传统CSS加严格规范也行。
最重要的是别让工具选择成为团队内耗的导火索。技术总监说过一句挺对的话:“选哪个方案不是最重要的,重要的是选完之后大家能不能一起把它用好。”

评论 0