💅 CSS-in-JS(CSS Modules → styled-components → 零运行时)
一句话定性
当前端进入组件化时代,样式遇到一个根本性的存在主义问题:样式到底属于谁? 全局 CSS 说它属于整个页面,但组件说它属于”我”。CSS-in-JS 给出了一个激进的答案——让样式和组件同生共死,把样式写进 JS,锁进组件作用域。它一度是 React 生态的主流,却最终在性能和 Server Components 的双重夹击下,被迫向”零运行时”回摆。
一、它是什么 & 出现的时代
CSS-in-JS 指的是用 JavaScript 来定义和管理组件样式的一类方案,兴起于 2013-2018 SPA时代,与 React 组件化深度绑定。它不是单一技术,而是一条不断演化的路线:
| 阶段 | 代表 | 时间 | 形态 |
|---|---|---|---|
| 局部作用域 | CSS Modules | 约 2015 | 仍写 .css,编译期把类名 hash 成唯一值 |
| 运行时 CSS-in-JS | styled-components / Emotion | 2016 起 | 用 JS 模板字符串写样式,运行时注入 <style> |
| 零运行时 | vanilla-extract / Linaria | 2020 起 | 用 JS/TS 写样式,编译期提取成静态 CSS |
二、为什么会出现:组件化时代,样式必须和组件绑在一起
全局 CSS 与组件化的三重根本矛盾
方法论(BEM)靠人自觉、不可靠;开发者想要技术上强制的解决方案。组件化时代,样式必须满足三个全局 CSS 天生满足不了的诉求:
- 作用域隔离:一个
<Button>组件的样式只能作用于它自己,绝不能泄漏出去污染别人,也绝不被外部污染。这正是全局作用域的反面。- 动态样式 / 与 props 联动:
<Button variant="danger" size="lg">的样式要根据 props 实时变化。这是运行时的需求——静态 CSS 和 预处理器(编译期死值)都做不到。- 依赖关系内聚:删掉一个组件,它的样式应该一起消失,而不是变成永远没人敢删的”死 CSS”。
CSS-in-JS 的核心主张:既然 React 已经把 HTML(JSX)和逻辑(JS)放进了组件,那样式凭什么还要游离在外?把三者合一,组件才真正”自包含”。 这是 UI = f(state) 思想在样式领域的自然延伸。
三、核心机制 & 为什么流行:三个阶段
阶段一:CSS Modules —— 用编译期 hash 实现”真·局部作用域”
你照常写 Button.module.css,但构建工具(Webpack/Vite)会把里面的 .button 编译成全局唯一的 Button_button_x7f9。你在 JS 里 import styles 引用它。
它优雅地解决了"作用域"
这是 BEM 想做的事(局部作用域)被编译器自动实现的版本:你不用再手写长类名,机器替你保证不冲突。它没有运行时开销,至今仍是非常稳健的选择。但它解决不了”动态样式/props 联动”——它本质还是静态 CSS。
阶段二:运行时 CSS-in-JS(styled-components 2016 / Emotion)—— 样式即组件
styled-components(Max Stoiber 等,2016)走得更激进,用 JS 模板字符串直接生成带样式的组件:
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
`;它为什么俘获了整个 React 生态
- 动态样式信手拈来:样式能直接读 props、读 theme、读任何 JS 变量——这是它对 CSS Modules 的碾压性优势。
- 真正的组件内聚:样式、结构、逻辑全在一个
.js文件里,组件成了完整的”样式 + 行为”单元。- 主题化 + 死代码自动消除:
ThemeProvider让换肤优雅;组件没了样式自然没了。在 2016–2020 年间,styled-components / Emotion 几乎是 React 项目的默认样式方案,代表着”组件化美学”的巅峰。
阶段三:零运行时(vanilla-extract / Linaria)—— 鱼与熊掌
后文会讲到运行时方案的代价。零运行时方案的机制是:让你依然用 JS/TS 的写法和类型安全写样式,但在编译期就把它们提取成静态 .css 文件,运行时不再有任何 JS 参与样式注入。Linaria、vanilla-extract 是代表。它试图保留 CSS-in-JS 的开发体验,去掉它的运行时成本。
四、带来的新问题 / 副作用:运行时方案的”原罪”
运行时 CSS-in-JS 的代价,几年后集中爆发
styled-components/Emotion 把样式注入放到了浏览器运行时,这带来一系列结构性问题:
- 运行时性能开销:每次渲染,JS 都要解析模板字符串、生成类名、序列化 CSS、插入
<style>标签。在大型应用里,这部分开销实实在在拖慢渲染(尤其频繁更新的组件)。- SSR 复杂且脆弱:服务端渲染时要”收集本次渲染用到的所有样式”再随 HTML 一起吐出,否则会出现 FOUC(无样式闪烁) 或 hydration 不匹配。各库都要写专门的 SSR 适配代码,复杂且易错。详见 渲染模式演进史。
- 额外的运行时库体积:样式引擎本身要打包进 bundle。
- 样式与逻辑过度耦合:有时只想调个颜色,却要动 JS 文件、触发组件重渲染。
压垮骆驼的最后一根稻草:React Server Components
RSC 与运行时 CSS-in-JS 根本不兼容
五、为什么会衰落 / 现状:向零运行时与原子化双向回摆
运行时方案退潮,样式方案分流
到 2023 年后,纯运行时 CSS-in-JS 明显退潮。社区分流向两个方向:
- 回摆到零运行时:vanilla-extract / Linaria 保留”用 JS/TS 写、类型安全、组件内聚”的好处,但编译期提取静态 CSS,既兼容 RSC、又无运行时开销。这是对运行时方案的”修正主义”。
- 彻底转向原子化:很多团队干脆放弃 CSS-in-JS,转向 Tailwind——既有作用域隔离的效果(utility 不会冲突),又是编译期生成、零运行时、天然兼容 RSC。
这条路线的深层启示
组件化:样式应属于组件,不应是全局的
│
▼
CSS Modules ──► 编译期 hash 实现局部作用域(解决"冲突")
│ └──► 但仍是静态 CSS,做不到 props 动态样式
▼
styled-components / Emotion(2016)──► 运行时注入, 动态样式 + props 联动
│ └──► 副作用:运行时开销 + SSR 复杂 + bundle 膨胀
│
├──► React Server Components 出现 ──► 运行时方案与 RSC 根本不兼容(致命一击)
│
▼
分流(2020+):
├─ 零运行时:vanilla-extract / Linaria(编译期提取, 兼容 RSC)
└─ 转向原子化:Tailwind(编译期生成, 零运行时)→ [[原子化CSS]]
🔗 同组:CSS演进史 | 布局演进史 | CSS方法论 | CSS预处理器 | 原子化CSS 🔗 相关:React | 渲染模式演进史 | Svelte | Vite | Webpack | 2013-2018 SPA时代 | 2018-2023 工程化时代 | UI组件库演进史