样式方案的选择往往在项目后期才显出分量。当一个基于JSS构建的React项目运行了两三年之后,你可能会发现首屏渲染时样式计算占用的时间越来越多,DevTools的Performance面板里频繁出现JSS运行时动态生成样式、批量插入style标签的调用栈。Stitches作为新一代CSS-in-JS库,采用接近零运行时的设计思路,在样式生成效率与包体积上都有明显优势。这篇文章就来聊聊迁移的动机、两套方案的核心差异,以及一次完整的改写实践。

一、为什么要离开JSS:性能差异到底在哪里
JSS诞生于CSS-in-JS早期,它的核心工作方式是纯运行时的:组件渲染时,JSS在JavaScript层面解析你写的样式对象,调用CSSOM接口(通常是insertRule)把规则逐条写入动态创建的style标签。这种方式在中小项目里几乎感知不到开销,但当组件数量上千、样式规则上万条时,问题就来了。
首先是首帧成本。JSS需要在组件挂载时完成样式对象的解析、类名生成、规则序列化三个步骤,全部发生在主线程上。如果页面一次性渲染大量组件,这段时间会直接体现在首次内容渲染的指标里。其次,JSS在样式对象变化时的diff逻辑并不总是增量友好的,某些动态样式更新会触发整条规则的删除与重建,而不是修改单条属性。
Stitches的思路则不同。它在开发模式下保留完整的运行时调试能力,但在样式解析上做了大量缓存优化:同样的样式对象只解析一次,生成的类名是基于样式内容hash的内容寻址类名,命中缓存时几乎零开销。此外,Stitches的API设计鼓励使用variants而不是内联样式对象,静态variants在首次使用后即被缓存,后续渲染直接复用类名字符串,连序列化这一步都省掉了。实测下来,一个中等规模的项目从JSS切换到Stitches后,样式相关的运行时开销大约降低了60%到80%,打包体积也缩小了约40KB(gzip后约12KB),收益相当可观。
二、核心API对照与组件改写实践
迁移最大的工作量在于改写样式定义。JSS的习惯是用createUseStyles返回一个hook,然后在组件里取类名:
import { createUseStyles } from 'react-jss';
const useStyles = createUseStyles({
button: {
padding: '8px 16px',
borderRadius: 6,
background: (props) => props.variant === 'primary' ? '#2563eb' : '#6b7280',
color: '#fff',
'&:hover': { opacity: 0.85 },
},
});
function Button(props) {
const classes = useStyles(props);
return <button className={classes.button}>{props.children}</button>;
}Stitches则推荐用styled直接创建带样式的组件,把动态部分交给variants系统:
import { styled } from '@stitches/react';
const Button = styled('button', {
padding: '8px 16px',
borderRadius: 6,
color: '#fff',
'&:hover': { opacity: 0.85 },
variants: {
kind: {
primary: { background: '#2563eb' },
secondary: { background: '#6b7280' },
},
},
defaultVariants: { kind: 'primary' },
});
// 使用时:<Button kind="secondary">取消</Button>对比可以看到两个关键变化。第一,JSS里用函数接收props来动态计算样式的写法,在Stitches中变成了声明式的variants,框架预先知道有哪些可能的取值,类名可以提前生成并缓存,这是性能优势的来源。第二,Stitches的styled组件本身就是React组件,不需要手动拼接className,也天然支持as属性做多态渲染,比如<Button as="a">可以把按钮渲染成链接元素。
如果某些样式确实无法用variants表达,比如跟随输入值连续变化的宽度,Stitches也支持函数式的动态值:styled('div', { width: (props) => props.w }),但这类写法会退化为运行时生成,应控制使用频率,避免为了一个动态值牺牲整块样式的静态性。
三、主题系统迁移与渐进式切换策略
主题是迁移中最容易踩坑的部分。JSS的主题通常依赖ThemeProvider配合一个普通JS对象,组件内通过useTheme读取后手动拼接进样式对象。Stitches内置了token化的主题系统,需要在createStitches时集中声明:
import { createStitches } from '@stitches/react';
export const { styled, css, globalCss, getCssText, theme } = createStitches({
theme: {
colors: {
primary: '#2563eb',
gray100: '#f3f4f6',
gray600: '#4b5563',
},
space: {
1: '4px',
2: '8px',
3: '16px',
},
fontSizes: {
sm: '13px',
md: '15px',
},
},
media: {
bp1: '(min-width: 640px)',
bp2: '(min-width: 1024px)',
},
});
// 使用 token:background: '$primary',间距:padding: '$3'这里有个容易混淆的点:Stitches的token引用语法是美元符号加token名,比如$primary,它会被编译成CSS自定义属性var(--colors-primary),运行时切换主题只需要在最外层挂不同的theme类名,无需重新生成任何样式规则。而JSS的主题切换往往意味着样式对象重新执行一遍,两者的更新成本完全不在一个量级。
至于迁移节奏,强烈建议渐进式进行,不要一次性重写。具体做法分三步:第一步,安装Stitches并在应用根节点完成createStitches初始化,把全局样式从JSS迁移到globalCss;第二步,按页面或业务模块为单位逐批改写组件,期间允许JSS与Stitches共存,两套方案各自维护样式互不干扰;第三步,改写完成后移除JSS依赖,做一轮完整回归。这样每个阶段都可独立发布验证,出现样式漂移时能快速定位到具体提交。
回归验证有几个实用技巧。可以在Storybook里为改写的组件建立用例,用快照对比迁移前后的DOM结构与类名;用Playwright跑关键页面的截图diff,重点关注hover、focus等伪类状态是否有遗漏。另外JSS的一些插件提供的默认行为(比如自动补px单位)在Stitches中不存在,务必全局搜索裸数字的属性值逐一补上单位。最后是服务端渲染的注意点:如果项目使用Next.js,Stitches提供了getCssText用于在SSR时收集样式并注入到HTML头部,避免首屏闪烁,配置比JSS的sheetsRegistry方案简单得多。整个迁移完成后,你会得到更快的首屏渲染、更小的打包产物,以及一套类型安全的样式API,这些收益在长期维护中会持续体现。