通过交付更多 CSS 提升 GitHub 站点性能
如今你在 GitHub 上看到的许多体验,都是由 Primer Design System 驱动的。从按钮到横幅再到面包屑导航,这些基础组件必须确保在多种场景下都具备无障碍性、灵活性和高性能。
2023年,某些页面上的组件数量开始爆发式增长。这给我们的现有 CSS-in-JS 方案带来了若干性能挑战:
- 由于样式在客户端初始化,导致首屏加载时间变长
- 随着样式收集逻辑从客户端迁移,服务端渲染(SSR)性能下降
- 随着页面组件数量增多,样式更新变得难以掌控
Primer 团队意识到,必须从根源上解决这个问题。我们需要寻找一种替代方案,完全规避当前方案在客户端和服务器端产生的成本。最关键的是,所选方案在迁移过程中绝不能导致 GitHub 出现任何功能故障。
引入 CSS Modules
Primer 团队找到了一个满足所有标准的方案:CSS Modules。这种格式让我们得以做最喜欢的事情:编写和使用原生 CSS 特性,同时保留一定程度的就近原则(colocation)和封装性,这也是我们过去对 CSS-in-JS 的期待。
使用 CSS Modules 后,样式将写在组件 JavaScript 源码旁边的 CSS 文件中。此外,该方案默认将所有类名视为局部变量,从而防止全局选择器引发的冲突和问题。这种格式还消除了任何客户端或服务器端运行时行为的需求。相反,样式会被打包进 CSS 样式表,作为页面 HTML 的一部分发送。
然而,这与当时我们使用的 CSS-in-JS 方案有巨大差异。这一变更需要更新所有 Primer 组件,以及 GitHub 上使用该技术编写的所有组件。幸运的是,设计系统是大规模交付此类变更的完美载体。
逐步迈向 CSS Modules
转向 CSS Modules 的方向已经很明确。Primer 团队需要更新各个组件,将它们从 CSS-in-JS 迁移到 CSS Modules。与此同时,我们对这些组件的更新不能破坏 GitHub 上任何现有的使用方式。最后,我们用于 CSS-in-JS 的底层技术也必须继续支持那些仍在使用的 GitHub 组件。
在面临这些约束的情况下,我们决定采取一种渐进式的迁移策略,以确保在安全地发布组件更新的同时,不破坏现有功能。对于每个组件,我们的计划如下:
- 新增一个文件,将现有样式转换为 CSS Modules
- 将该组件加入特性开关(feature flag),以便在新旧样式之间切换
- 利用现有的视觉回归测试,验证 CSS-in-JS 和 CSS Modules 之间的快照是否一致
- 逐步向团队、GitHub 员工以及最终所有 GitHub 用户开放特性开关,以便及时发现过程中的任何问题
这一过程形成了一个强有力的反馈循环,Primer 在向 GitHub 持续交付这些变更的过程中,能尽早暴露问题。使用特性开关让我们能够安全地执行此次迁移,同时也让我们清晰地看到了 CSS Modules 带来的性能收益。
到 2024 年 12 月,Primer 中的所有组件都已通过这一流程迁移至 CSS Modules。我们在全方位的性能指标上都看到了提升,特别是:
- 服务端渲染页面的耗时减少了 55%
- 页面上组件初始化的耗时减少了 25%
在 Primer 中通过这项工作取得了明确的性能提升后,我们开始思考是否能在 GitHub 的其他部分通过类似的转换获得相似的性能收益。同样,我们也在问,公司多久才能彻底停止对 CSS-in-JS 的支持?
在 GitHub 中逐步淘汰 CSS-in-JS
从 Primer 中移除 CSS-in-JS 解决方案最棘手的地方之一,在于 sx 属性的使用。这个属性是 主要用于样式化和定制 Primer 组件的方式。团队可以通过传入内联对象来定制组件的所有方面。它代表了 CSS-in-JS 最好也最坏的方面:
- 优秀的 TypeScript 支持,并与我们的 Design Tokens 集成
- 与组件共置(co-located),所有代码都集中在一处
- 由于
sx使用的内联对象具有动态特性,运行时开销较高 - 随着页面上使用
sx的组件越来越多,扩展变得困难
因此,我们告别 CSS-in-JS 的第一步,就是减少 sx 在 GitHub 代码库中的使用。这能让我们立即获得性能提升,效果类似于迁移 Primer 组件时看到的收益,同时也为彻底移除 CSS-in-JS 打下了基础。
Primer 的两面性
需要注意的是,虽然设计系统本身已经正式脱离了 styled-components,但 GitHub 代码库的很大一部分还没有。由于 sx props 多年来一直是 GitHub 事实上的样式标准,我们面对的是成千上万个需要迁移的 sx props——只有先处理完这些,才能让 GitHub 用上不依赖 styled-components 的全新 @primer/react 版本。
那么……我们是如何在完成如此庞大的工作的同时提升信心、降低风险的呢?答案是:不要一次性完成。
最初的 CSS 迁移其实比我们上面说的更复杂一些:除了把组件迁移到 CSS modules、通过 feature flag 在生产环境测试并逐步发布之外,我们还在一个间接依赖库里创建了一系列“包装”组件,称为 @primer/styled-react。这个包的目的就是让新迁移的组件仍然支持 sx。这样一来,GitHub UI 代码库中使用该 prop 的地方可以通过 @primer/styled-react 导入同名组件继续使用它,而不使用的地方则可以直接从 @primer/react 导入,从而享受性能提升。
Styled Box 归零计划
迁移的下一阶段如下:
- 逐个包进行处理:
- 把所有
sx用法翻译成等价的 CSS modules 文件,包括交叉引用(参见迁移到 CSS 变量) - 把
@primer/styled-react的导入替换为@primer/react - 在预发布环境测试
- 部署上线
- 把所有
有趣的是,正当我们准备投入这项大规模工作时,styled-components 宣布进入维护模式,这进一步印证了我们迈出的方向是正确的。
这项工作于 2025 年 4 月启动,当时约有 7,760 个待迁移的 sx 属性;直到 2026 年 5 月才彻底完成。起初,我们内部优秀的开发者之一、Ian Sanders,开发了一款VS Code 插件,用于辅助按属性进行迁移。同时,内部还开发了一个类似的 codemod 工具,用于在 GitHub 代码库中整文件迁移。这项大部分实现自动化的工作,仍需一定的人工监督和仔细验证。在一个 6 月的周期内,8 名工程师轮班迁移了 6,419 个属性,部分页面的服务端渲染时间性能提升从 1% 到 22% 不等。

在 GitHub 的另一端,Copilot 的能力呈指数级增长。AI 变得更聪明、更强大;在这项工作进行期间,我们发布了 Copilot coding agent 和 Copilot code review。
当我们于 2026 年 4 月重新接手这项工作时,局面已变;两名工程师凭借坚定的决心和大量的 Copilot coding agent,在三周内将 sx 属性数量从 895 个减少到 0 个。

Battle of the themes
那天是个大日子:我们终于完成了 sx 的迁移,而这也是彻底移除 styled-components 前最后一道门槛,这事儿筹划了好几年……终于可以清理这些依赖,转向其他更有趣的工作了,对吧?大错特错!
GitHub 支持七种不同的主题,每种都提供高对比度模式。而实现这一切,靠的正是 styled-components。在考虑移除这些依赖之前,我们得先把主题系统与它解耦。
不过,事情并没有听起来那么棘手。我们的主题变量一直通过 @primer/css 包以 CSS 形式定义,早在 2025 年底迁移 @primer/react 时,我们就为不依赖 styled 组件的主题方案预留了路径。真正需要移除的,是 styled-components 带来的那些 JavaScript 用法和工具函数。于是,我们又动手了。
流程你们应该很熟悉了:执行迁移,小流量灰度,给所有功能加上开关(feature flag)。两个月过去,中间偶尔有些小状况,但最终还是所有系统就绪,可以移除依赖了——即便移除依赖这个动作本身也加了功能开关。谨慎点总比后悔强。
结局圆满
截至 2026 年 6 月,GitHub 已完全运行在 100% CSS modules 之上。我们布下的安全措施,让大规模架构变更得以安全上线、在生产环境做压力测试、及时捕获错误并高效修复,最终达成目标,顺带收获了显著的性能提升。
起初看似只是把 CSS 迁移过去,最终却演变为 GitHub 在规模化场景下对样式、主题和 UI 交付方式的逐步重构。到最后,我们不仅从 dotcom 移除了 sx、styled-components 和 styled-system,而且全程没有弄崩 GitHub。在 GitHub,持续提升产品性能、用户体验和 delight(惊喜感),始终是我们最重要的事。