GitHub 对 github.com 样式体系的改造,不是一次简单的语法替换。迁移从 2023 年组件数量快速增长带来的性能问题开始,持续三年;按照复盘给出的终点,到 2026 年 6 月,网站已经完全运行在 CSS Modules 上,并删除了 styled-components、styled-system 以及大量 sx props。
这场迁移最值得关注的地方,不是“CSS Modules 战胜了 CSS-in-JS”,而是一个大型前端系统如何重新划分样式的执行时机、组件边界和长期维护成本。
问题不只在 CSS,而在运行时成本的放大
GitHub 的 UI 大量建立在 Primer 设计系统之上。当组件数量持续增长时,样式方案中的固定成本也会被一起放大:组件需要解析 props、计算样式、生成或组合类名,并处理主题与样式覆盖。
这并不意味着所有 CSS-in-JS 都一定慢。真正需要检查的是,应用是否把本可在构建阶段完成的工作,留到了浏览器运行阶段。在大型页面中,一点点组件级开销乘以成百上千个实例,就可能进入可观测范围。
CSS Modules 改变了这条执行路径:
- CSS 在构建阶段被拆分和处理,而不是依赖组件挂载时生成规则。
- 组件通过普通
className引用样式,浏览器直接使用成熟的 CSS 匹配机制。 - 类名默认局部化,降低全局命名冲突风险。
- 媒体查询、伪类和动画回到原生 CSS,不需要全部包装成 JavaScript API。
- 删除样式运行时后,依赖图、客户端代码和排查路径都更简单。
因此,这次迁移改变的是网站的“运行性格”:样式从组件执行过程中的动态参与者,变成更接近静态资源的构建产物。
真正困难的是替换 sx 带来的便利
styled-components 这样的依赖可以从 package.json 中删除,但 sx props 往往已经渗透到业务组件内部。它允许开发者在调用处快速写间距、颜色和响应式规则,也容易让设计决策散落在 JSX 中。
迁移到 CSS Modules 后,团队必须回答几个具体问题:
- 高频样式组合应该成为组件变体,还是保留为工具类?
- 主题值如何传递,使用 CSS 自定义属性还是生成多套样式?
- 真正的动态值如何处理,而不重新发明一个 CSS-in-JS 系统?
- 调用方是否还能随意覆盖组件内部结构?
合理的边界通常是:有限状态使用类名,主题使用 CSS 变量,只有真正来自运行时的数据才进入内联样式。 例如按钮的尺寸和语义属于有限变体,而进度条的实时百分比才是动态值。
可以这样迁移一个 React 组件
下面是一个可放入 React 与 TypeScript 项目的最小示例。假设构建工具已经支持 CSS Modules;Vite、Next.js 等常见工具通常具备这项能力。
迁移前,组件可能把样式和 props 计算都交给 styled-components:
import styled from 'styled-components';
const Button = styled.button<{ $danger?: boolean }>`
border: 0;
border-radius: 6px;
padding: 8px 12px;
color: white;
background: ${({ $danger }) => ($danger ? '#cf222e' : '#1f883d')};
&:hover {
filter: brightness(0.92);
}
`;
export function DeleteButton() {
return <Button $danger>Delete repository</Button>;
}
迁移后,将有限的视觉状态改为 CSS 类。创建 Button.module.css:
.button {
border: 0;
border-radius: 6px;
padding: 8px 12px;
color: var(--button-fg, #ffffff);
background: var(--button-bg, #1f883d);
cursor: pointer;
}
.button:hover {
filter: brightness(0.92);
}
.default {
--button-bg: #1f883d;
}
.danger {
--button-bg: #cf222e;
}
.button:focus-visible {
outline: 2px solid #0969da;
outline-offset: 2px;
}
再创建 Button.tsx:
import type { ButtonHTMLAttributes } from 'react';
import styles from './Button.module.css';
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'default' | 'danger';
};
export function Button({
variant = 'default',
className = '',
...props
}: ButtonProps) {
const classes = [styles.button, styles[variant], className]
.filter(Boolean)
.join(' ');
return <button className={classes} {...props} />;
}
export function DeleteButton() {
return <Button variant="danger">Delete repository</Button>;
}
这段代码保留了调用方传入 className 的能力,但把核心变体收回组件内部。若团队不希望外部覆盖样式,也可以移除公开的 className,进一步收紧边界。
真正动态的值则可以通过 CSS 自定义属性传递。例如进度条不需要为每个百分比创建一个类:
import type { CSSProperties } from 'react';
import styles from './Progress.module.css';
export function Progress({ value }: { value: number }) {
const safeValue = Math.max(0, Math.min(100, value));
const style = { '--progress': `${safeValue}%` } as CSSProperties;
return (
<div className={styles.track} aria-label="Progress">
<div
className={styles.bar}
style={style}
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={safeValue}
/>
</div>
);
}
对应的 Progress.module.css:
.track {
width: 100%;
height: 8px;
overflow: hidden;
border-radius: 999px;
background: #d0d7de;
}
.bar {
width: var(--progress);
height: 100%;
background: #1f883d;
transition: width 160ms ease;
}
@media (prefers-reduced-motion: reduce) {
.bar {
transition: none;
}
}
这里的关键不是禁止 style,而是避免用它表达本来有限、稳定且可静态分析的设计状态。
三年迁移说明“大爆炸重写”通常不可取
大型站点很难暂停功能开发,再一次性重做整个样式层。更稳妥的路径是允许新旧方案短期共存,同时持续压缩旧系统的使用范围。
可以先用下面的命令建立依赖和调用点清单;请按仓库目录调整 src:
# 查找直接依赖和导入
rg -n "styled-components|styled-system" package.json src
# 查找 sx props;该表达式只是初筛,仍需人工确认
rg -n "\bsx=|\bsx:\s*" src
# 统计 CSS Modules 文件数量
find src -type f \( -name "*.module.css" -o -name "*.module.scss" \) | wc -l
随后可以按以下顺序推进:
- 冻结新增债务:Lint 或代码评审禁止新增 styled-components 和
sx。 - 先迁移叶子组件:按钮、标签、图标容器等依赖较少,更容易验证视觉一致性。
- 提炼稳定变体:把散落的颜色和间距覆盖转成
size、variant、density等受控 API。 - 建立视觉回归测试:CSS 迁移最常见的风险不是编译失败,而是层叠顺序、焦点状态和响应式布局悄悄变化。
- 观察性能而非只看文件数:同时检查 JavaScript 体积、渲染时间、样式重计算和真实用户指标。
- 最后删除运行时:只有搜索结果、构建产物和线上监控都确认没有旧调用后,才移除依赖。
在清理阶段,可以把残留检查放进 CI:
#!/usr/bin/env bash
set -euo pipefail
if rg -n "styled-components|styled-system|\bsx=" src package.json; then
echo "Legacy styling API detected. Use CSS Modules or an approved replacement."
exit 1
fi
echo "No legacy styling API found."
是否应该跟随 GitHub 迁移
答案取决于瓶颈,而不是技术潮流。如果应用规模较小、动态主题复杂,并且现有 CSS-in-JS 方案已经做了静态提取,那么迁移收益可能不足以覆盖改造成本。反过来,如果样式运行时进入性能剖析热点、组件 API 被 sx 覆盖淹没,或者客户端承担了大量可在构建阶段完成的工作,CSS Modules 就值得认真评估。
落地前可以检查四件事:
- 是否有真实性能数据证明样式系统值得改造?
- 设计令牌、主题和响应式规则能否用 CSS 变量与原生 CSS 表达?
- 是否能让新旧方案共存,并按组件逐步迁移?
- 是否具备视觉回归、可访问性和线上指标监控?
GitHub 的三年迁移给出的核心经验并不是“选择某个更时髦的库”,而是把样式重新放回合适的执行阶段。CSS Modules 提供的价值,也不仅是局部类名,而是让浏览器负责 CSS,让 JavaScript 更专注于交互与状态。