我在通勤地铁上想通的CSS方案选型这件事

SQL调音师
2026-08-21 20:47
阅读 248

先说结论:没有银弹。做中大型前端项目、团队混合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

最热最新
暂无评论
SQL调音师Lv.1
0
影响力
0
文章
0
粉丝