GitHub 花了很长时间,把 Primer 从 CSS-in-JS 里整个搬了出来。2024 年 12 月,最后一批组件迁移完成,账面上的数字很直白:服务端渲染时间少了 55%,组件初始化时间少了 25%。负责这件事的工程师 Josh Black 把过程写在了 GitHub 官方博客上。这不是一次追时髦的技术换代,而是一场被性能数据逼出来的架构手术——而且它的做法,和大多数人想象中"推倒重来"完全不一样。
CSS-in-JS 的账单,最终都摊在首屏
运行时不是免费的
styled-components 这类方案的思路,是把样式表塞进 JavaScript。写起来确实舒服:组件和样式住在同一个文件里,props 一变,样式跟着变,不用在模板和样式表之间来回跳。代价藏在运行时——浏览器里始终躺着一套样式引擎,每次渲染都要把 props 插值成 CSS 字符串、算哈希、生成新类名、再注入文档。
几十个组件的时候,这点开销可以当它不存在。可 Primer 是 GitHub 全站的设计系统,几百个组件散落在成千上万个页面里。这笔钱是按渲染次数收的,规模一大就疼。
服务端渲染把成本又放大一轮
页面上了服务端渲染,问题换了个地方发作。服务端要先把组件渲染成 HTML,顺手把样式收集起来、序列化,再塞回文档。用户拿到的第一屏里,混着大量只为这一页服务的样式文本。
于是出现一个挺荒诞的局面:同一个按钮、同一张卡片,在十个页面里被内联了十遍。浏览器缓存插不上手,JS 包也不会因为样式复用了而缩小。首屏变慢,而且是慢在服务器和网络上。
设计系统越大,转身越难
GitHub 团队清楚,这不是换个 styling 库那么简单。Primer 的样式和组件的 props、主题、断点逻辑缠在一起,牵一发动全身。真正难的地方从来不是写 CSS,而是找到一条能一边发货、一边换引擎的路。
迁移能跑起来,靠的是"两套并存"
不做大爆炸式重写
他们没有拉一条迁移分支,把功能开发停上几个月。更实际的做法是在 CSS Modules 和 styled-components 之间架一层薄桥:新写的组件走新路子,老组件原封不动,两边共用同一套设计令牌。任何一个组件都可以单独搬,搬完立刻上线。
迁移的节奏因此由业务决定,而不是由架构决定。这一点听着不酷,但它决定了这件事能不能真的做完。
把设计令牌沉到 CSS 变量里
真正的地基,是让颜色、间距、字号、圆角这些设计令牌变成 CSS 自定义属性,由构建流程统一生成。主题切换由此从"在 JavaScript 里换一套对象",变成"根节点上换个属性",剩下的交给浏览器重算。样式层和 JS 层到这一步才算分开,后面所有的性能收益都以它为前提。
一个组件,一次验收
几百个组件,就是几百次小验收。团队拿真实页面做基准,比较迁移前后的渲染耗时、HTML 体积和交互延迟。数字没变好,就不继续往下推。这种近乎笨拙的节奏,反而让一个横跨全站的改造没有失控。
那两个百分比,拆开看更清楚
服务端少算的那一遍
55% 的提速,省下来的是重复劳动。样式不必在每次请求里重新求值、重新序列化,更多内容变成了静态资源,服务器只负责拼装 HTML。请求量越大,省下的绝对时间越多。这不是什么玄学优化,只是把一份本该被复用的东西真正复用了起来。
客户端省下的过路费
组件初始化快了 25%,原因很朴素:客户端不用再为每个实例跑一遍样式计算、生成类名、注入样式表。挂载路径短了,用户感知到的就是快。这类收益往往不体现在某个大数字上,而是散落在每一次点击、每一次页面切换里。
缓存终于站在了正确的一边
还有一项不容易写进标题的收益:样式从内联文本变成了可长期缓存的文件。缓存命中率上去了,文档体积下来了,首次访问和再次访问之间的差距被拉开。对 GitHub 这种靠页面数量取胜的站点,这种收益比任何单点优化都更耐看。
想抄这份作业,先算清楚自己的账
共存期要靠纪律,不是靠默契
两套方案并存的时间可能长达一两年。没有硬性约定,新组件很快会顺着惯性长回旧样子。命名规范、作用域边界、代码评审清单,这些枯燥的东西决定了迁移到底会不会烂尾。
CSS 的全局性得先被驯服
CSS Modules 解决的是作用域问题,不是全部问题。优先级、层叠顺序、样式复用的边界,仍然要人来管。Primer 团队把大量精力放在让类名可预测、让层级可控上——这件事做不好,性能收益很快会被维护成本吃掉。
有些项目确实不该动
如果组件数量有限、动态样式占比很高、团队只有两三个人,那 CSS-in-JS 带来的开发体验,很可能比这点性能收益更值钱。GitHub 有动力迁移,是因为它背后有几千个页面、几十条产品线,以及一个必须保持统一的设计系统。规模不到那个量级,照搬这套路径只会给自己找麻烦。

