多发 CSS 反而更快:GitHub 用三年把 7760 个 sx 属性清零
GitHub 在 9 月 25 日发了一篇标题很别扭的工程复盘:《Improving site performance by shipping more CSS》——多发 CSS,站点反而更快。
数字是硬的:Primer 设计系统全量迁到 CSS Modules 后,单页服务端渲染时间少了 55%,组件初始化时间少了 25%;到 2026 年 6 月,github.com 上的 styled-components、styled-system、sx prop 全部清零,全站 100% CSS Modules。
先给结论:这不是“CSS 比 JS 好”的站队题,而是样式计算发生在什么时机的问题——运行时每次渲染算一遍,还是构建期算一次、之后交给浏览器缓存。GitHub 选了后者,代价是三年时间、7760 个 sx 属性,以及一整套灰度机制。
2023 年那三笔账
2023 年,GitHub 某些页面上的组件数量开始爆炸。原来的 CSS-in-JS(styled-components)方案同时欠了三笔账:
- 首屏:样式在客户端初始化,用户先看到没有样式的结构,再等 JS 把样式注进去;
- 服务端渲染:样式收集从客户端挪回服务端,SSR 每渲染一次就要走一遍组件树收集样式声明,耗时随组件数线性涨;
- 样式更新:一个页面上组件越多,每次状态变化触发的样式重算越失控。
三笔账的共同点是成本跟着组件数走,不跟着样式字节数走。CSS-in-JS 的运行时是 O(组件数)的,静态样式表是 O(1)的——解析一次、缓存命中,之后跟组件多少无关。
拆到浏览器这一层,差别更清楚。静态样式表在 HTML 解析阶段就被拿去建 CSSOM,渲染流水线提前知道哪些规则会用在哪;运行时注入的样式要等 JS 执行到那一行才出现,注完还会触发一次样式重算(style recalculation),组件越多,这次重算要扫的节点越多。服务端渲染同理:Node 得先把整个组件树跑一遍,把每个组件产出的样式字符串收集起来拼成 <style>,这个收集动作本身就是 SSR 耗时的一部分。
换句话说,CSS-in-JS 把“写样式”的便利留在了开发期,把账单挪到了每次渲染。规模小的时候这张账单看不出来,规模上去之后它跟着组件数一起涨。
迁移没有一次性切换
GitHub 没有立 flag 重写。每个组件走四步:
- 新增一个文件,把原样式翻译成 CSS Modules;
- 给这个组件挂 feature flag,新旧两套样式随时切;
- 用已有的视觉回归测试比对快照,新旧两套必须像素级一致;
- 放量顺序:Primer 团队 → GitHub 员工 → 全量用户。
到 2024 年 12 月,Primer 所有组件迁完,收益落地:SSR 少 55%,组件初始化少 25%。
难点在组件之外。sx 是 GitHub 内部给 Primer 组件传样式的方式,本质是一个内联对象:
// before:运行时把对象编译成规则,每次渲染都要算
import { Box } from '@primer/react';
<Box
sx={{
display: 'flex',
gap: 2,
padding: 3,
borderRadius: 2,
borderColor: 'border.default',
borderStyle: 'solid',
}}
>
{children}
</Box>
它的优点是 TypeScript 类型提示和 Design Token 直连,缺点是每次渲染都要在运行时把对象变成 CSS 规则、生成类名、注入 DOM——服务端注入一次,客户端水合再注入一次。
// after:构建期就把类名和规则算死,浏览器拿到就能用
import { Box } from '@primer/react';
<Box
className={styles.Box}
>
{children}
</Box>
/* Box.module.css -- 类名默认局部作用域,规则随 HTML 一起发出去 */
.Box {
display: flex;
gap: var(--space-2);
padding: var(--space-3);
border: 1px solid var(--borderColor-default);
border-radius: var(--borderRadius-2);
}
为了让两套写法长期共存,团队做了一个包装层 @primer/styled-react:还写 sx 的代码继续从这个包引组件(内部转到新的 CSS Modules 组件),不用 sx 的直接从 @primer/react 引。性能收益先吃到,债务慢慢还——这是整件事里最值得抄的设计。代价也明摆着:迁移期内仓库里同时跑着两套样式体系,新旧快照要对、开关要能回滚、每一次合并都要确认自己站在哪一边。三年就是这么花掉的。
7760 个 sx 属性怎么清零的
- 2025 年 4 月:存量约 7760 个
sx属性,迁移启动。工程师 Ian Sanders 写了个 VS Code 插件做单属性转换,另有一个内部 codemod 处理整文件; - 前 6 个月:8 名工程师轮换,迁掉 6419 个,部分页面 SSR 提升 1%~22%(注意区间,收益按页面差异极大);
- 2026 年 4 月:剩 895 个。2 名工程师 + Copilot coding agent,三周清零;
- 2026 年 6 月:github.com 100% CSS Modules,
sx、styled-components、styled-system 全部移除。
同期还发生了一件外部事件:styled-components 官方宣布进入维护模式。方向对不对,不用争论了。
主题是最后一关
清完 sx 还不够——GitHub 支持 7 套主题,每套还有高对比变体,整套主题切换逻辑挂在 styled-components 的 JS 工具上。好在主题变量早就定义在 @primer/css 的 CSS 里,要摘的只是 JS 那层。两个月、全程 feature flag、连“依赖移除”这个动作本身都上了开关,才敢动手删依赖。
时间线
| 时间 | 事件 | 结果 |
|---|---|---|
| 2023 | 组件爆炸,CSS-in-JS 成本失控 | 立项 |
| 2024-12 | Primer 全组件迁完 | SSR -55%,初始化 -25% |
| 2025-04 | sx 迁移启动(7760 个) | – |
| 2025 年内 | 8 人 6 个月 | 迁 6419 个,SSR +1%~22% |
| 2026-04 | 重启收尾 | 2 人 3 周 + Copilot,895 → 0 |
| 2026-06 | 主题解耦完成 | 全站 100% CSS Modules |
Hacker News 上没被说服的人
这篇复盘在 HN 上炸出了一场争论,抓的点很实在:github.com 的 CSS 体积还在 2.1MB 量级,有人质疑页面并没有因此变快。
这个质疑该被认真对待,因为官方公布的只有 SSR 时间和组件初始化时间两项,没有公布 FCP、LCP、缓存命中率这些用户侧指标——客户端那一半故事是空的。把 55% 当成“网站快了 55%”是误读,它是服务端渲染一段代码的耗时下降。
HN 上还有一条更细的批评:生产包里的类名仍是开发用的长格式(类似 DirectoryContent-module__Box_3__gl6dE),明明可以压成短哈希,白白多占体积。这类意见不否定结论,但提醒了一件事——架构迁移完成,不等于 CSS 体积的优化工作就跟着完成了,两者是两条不同的轨道。
所以正确的读法是:GitHub 证明了“运行时样式计算是瓶颈”,没证明“CSS 越多越好”。字节数和计算时机是两个独立变量,这次被优化的是后者。
你的项目能抄什么
能抄的(跟规模无关):
- 灰度三件套:feature flag 切新旧 + 视觉回归测试比快照 + 团队→员工→全量的放量顺序。没有这套东西,任何架构迁移都是赌博;
- 双轨兼容层:一个包装包让新旧两套 API 长期共存,调用方按自己的节奏迁,性能收益先落袋;
- 债务量化:先数清楚有多少个
sx,按目录/团队建燃尽图,再动手写 codemod。
抄不了的: GitHub 有一个成熟设计系统当载体——包装层、codemod、视觉回归基线全都是现成的。没有设计系统的代码库,先别谈迁移。
还有一点得说清楚:这篇复盘不等于给 CSS-in-JS 判了死刑。Emotion、styled-components 在组件几十个、没有服务端渲染的项目里依然省事,它们按 props 动态算样式的本事也是真需求。GitHub 非删不可,是因为它的页面规模已经大到让这部分动态能力付不出对应的运行时成本。你的项目要不要迁,取决于你的组件数曲线往哪走,不取决于 GitHub 怎么选。
下一步
- 先量,别先改。 用下面这段把你的 CSS-in-JS 债务数出来,能数出数字才知道要迁多久:
# 统计 sx / styled-components 用量,按目录出燃尽图
grep -rIl --include='*.tsx' -e "sx={" src/ | wc -l
grep -rI --include='*.tsx' -c "from 'styled-components'" src/ |
awk -F: '{s+=$NF} END {print "styled-components 引用:", s}'
- 在服务端量一次 before。 找一个组件数最多的页面,量 SSR 单次渲染耗时,同时把该页面的组件数记下来,之后就能换算成「每百个组件的 SSR 毫秒数」,横向对比才有意义。没有基线就没有故事,55% 这种数字只有对着基线说话才算数。
- 给下一个要改的组件挂 flag。 不用等整体方案,先让一个组件的新旧样式可以一键切换、可以回滚——这套开关搭好之后,后面所有迁移都是重复劳动。
- 把视觉回归测试补上再谈迁移。 没有“新旧快照必须一致”这条硬门槛,灰度只是把风险往后推。哪怕先只覆盖几个核心页面的截图比对,也比纯靠人眼点一遍可靠。
参考:
- GitHub 官方复盘:Improving site performance by shipping more CSS(github.blog,2026-09-25,Josh Black / Marie)
- CSS Modules 规范:github.com/css-modules/css-modules
- sx → CSS 转换插件:VS Marketplace(ian-sanders.sx-to-css)
评论区
登录后可评论。