“少发 CSS、多用 JavaScript”并不天然更快。组件数量上升后,CSS-in-JS 会把样式生成、收集和更新变成客户端与服务端的运行时成本。GitHub 的经验说明,架构迁移的关键也不是选中 CSS Modules,而是让新旧系统可以长期并存、测量和回滚。
发生了什么
GitHub 9 月 25 日公开了 github.com 的样式迁移复盘。Primer 设计系统从 CSS-in-JS 转向 CSS Modules 后,官方测得页面服务端渲染时间减少 55%,页面组件初始化时间减少 25%。随后团队把迁移范围扩展到 GitHub 主站,最终在 2026 年 6 月实现 100% CSS Modules。
这不是一夜替换。2025 年 4 月存量一度约有 7760 个 sx 属性;8 名工程师在 6 个月内迁移 6419 个,不同页面的服务端渲染改善为 1%—22%。到 2026 年 4 月,剩余 895 个在两名工程师与 Copilot coding agents 协作下用三周清零。厂商数据没有给出完整实验区间与所有页面分布,因此应把它当作 GitHub 自身架构结果,而不是任何项目的保证。
技术原理:为什么“多一点CSS”可能更快
CSS-in-JS 的价值是类型提示、组件共置和动态表达,但运行时需要把对象转换成规则、生成类名、收集服务端样式并处理更新。CSS Modules 在构建期把局部类名编译进静态样式表,浏览器按擅长的路径解析和缓存;JavaScript 少做工作,即使传输的 CSS 稍多,总交互成本仍可能下降。
flowchart LR
A[盘点sx与旧依赖] --> B[组件改写CSS Modules]
B --> C[兼容包装层]
C --> D[视觉回归测试]
D --> E[功能开关灰度]
E --> F{性能与错误达标?}
F -->|否| G[回滚并修复]
F -->|是| H[扩大流量]
H --> I[删除旧运行时]
真正有价值的是双轨设计。Primer 先让组件本身使用 CSS Modules,又通过 @primer/styled-react 包装层继续接收旧 sx 属性。调用方可以分包迁移,设计系统先获得部分收益,旧代码也不会同时爆炸。每个组件配合视觉快照、预生产和功能开关逐步放量,迁移因此变成一系列可逆的小变更。
最小实践:先把迁移债务量化
不要先跑自动改写。下面的 Node.js 脚本先统计 sx 和旧包装包导入,便于按目录、页面或团队建立燃尽图。真实项目可把内存样本替换为文件遍历。
const files = {
"old/Button.tsx": `import {Box} from '@primer/styled-react';
export const A=()=> <Box sx={{p:2}}><Box sx={{m:1}} /></Box>`,
"new/Card.tsx": `import styles from './Card.module.css';
export const C=()=> <div className={styles.card} />`,
"mixed/Nav.tsx": `import {NavList} from '@primer/styled-react';
export const N=()=> <NavList sx={{gap:2}} />`,
};
let totalSx = 0;
let legacyFiles = 0;
for (const [name, source] of Object.entries(files)) {
const sx = (source.match(/\bsx\s*=/g) || []).length;
const legacy = source.includes("@primer/styled-react");
totalSx += sx;
legacyFiles += Number(legacy);
console.log(`${name}: sx=${sx}, legacy=${legacy}`);
}
console.log({totalSx, legacyFiles});
if (totalSx !== 3 || legacyFiles !== 2) process.exit(1);
依赖只有 Node.js。保存为 style_inventory.js 后运行 node style_inventory.js。本次使用 Node.js 26.7 实际运行,统计出 3 个 sx 属性、2 个旧包装包文件,退出码为 0。它不是 AST(抽象语法树)级 Codemod,字符串、注释和别名导入会产生误差;生产盘点应使用 TypeScript 解析器,并把结果抽样复核。
普通团队最该抄什么
第一,先定义性能预算,不要把“移除某库”当目标。至少观察服务端渲染、交互启动、样式更新、CSS 与 JavaScript 传输量。第二,把迁移单位控制在组件或包级,确保每一步能独立回滚。第三,自动改写只负责机械转换;响应式规则、主题变量和可访问性状态必须由视觉回归与人工审查兜底。
主题系统往往是最后一公里。GitHub 支持七种主题及高对比变体,移除 styled-components 前还要把 JavaScript 主题工具解耦为 CSS 变量。Primer 文档也提醒,CSS 变量不能直接出现在媒体或容器查询表达式中,一些 sx 值并非一对一映射。看到编译通过不等于视觉语义保持不变。
适用边界与风险
高组件密度、服务端渲染明显、主题稳定的产品最可能受益。若样式高度依赖每帧动态数据,或团队规模很小、运行时开销未进入性能剖析前列,迁移成本可能大于收益。Copilot 可加速存量改写,但不能替代功能开关、快照基线和真实用户指标。
指标比较还要避免两个陷阱。其一,只看 JavaScript 包体积,却忽略浏览器解析 CSS、缓存命中和首屏关键样式;其二,把实验组与不同页面、不同流量时段直接比较。更稳妥的做法是在同一页面、同一主题和近似设备条件下做灰度,分别观察服务端渲染、最大内容绘制、交互延迟、样式计算和错误率,再按高分位数判断。
自动化改写也应设“停止线”。遇到条件表达式、动态 token、伪类组合或主题分支时,Codemod 可以生成待办而不是猜一个 CSS。每个转换提交保留旧路径开关和视觉差异图,确认无回归后再合并下一批。这样做看似慢,却能避免把几千个机械变更堆成一个无人敢审的超大 PR。
如果团队正在大量使用 AI 生成前端代码,还应把新规则写进脚手架、Lint 和组件示例,阻止旧样式入口继续增长。迁移燃尽图只统计减少量会误导决策;同时记录新增量,才能看出团队是在还债,还是一边迁移一边制造新债务。
我的判断是,这次案例最值得复用的不是“CSS Modules 胜出”,而是迁移架构:兼容层买时间,指标决定节奏,开关负责止损,最后才删除旧路径。今天可以先运行一次存量盘点,并挑一个高流量、低交互风险的组件做 A/B,而不是开一个全仓重写分支。
你的前端项目中,样式方案最大的成本来自运行时、迁移风险,还是主题系统?
关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。
本文首发于 java4u.cn,转载请注明出处。