运行时CSS-in-JS方案最大的软肋在于"运行时"三个字:浏览器必须先下载并执行JavaScript,才能把样式插入到文档里,首屏渲染因此被拉长,交互过程中的样式更新也会占用主线程。GenieJS作为典型的运行时方案,样式生成逻辑全部跑在客户端,页面越复杂代价越明显。Linaria的思路完全不同,它把样式计算挪到了构建阶段,最终产物就是普通的CSS文件,运行时几乎零开销。这篇文章就来聊聊怎么把一个React项目从GenieJS平滑迁移到Linaria。

先弄清楚两套方案的底层差异
GenieJS这类运行时方案的执行链路是:组件渲染时调用样式函数,生成一份带哈希类名的样式对象,再通过style标签或CSSOM API动态插入文档。这个过程的优点是动态性极强,样式可以直接读取组件的state和props,想怎么变就怎么变;缺点是每次渲染都可能触发样式计算与DOM操作,SSR场景下还需要额外做样式提取与注入,复杂度不低。
Linaria则把这套逻辑搬到了编译期。它的Babel插件会在构建时静态分析你写的styled组件和css模板字符串,把样式抽出来生成真实的CSS文件,类名则被替换成不可预测的哈希值。组件代码里最终只剩下一个普通的className字符串,没有任何样式运行时。这意味着最终产物更小、渲染更快,SSR也变得非常简单——直接把生成的CSS文件通过link标签引入即可。
代价也很明确:Linaria要求样式在编译期可静态确定。想在样式里直接读JavaScript变量没问题,但变量必须是编译期常量;运行时才能确定的值需要换一种方式表达,这是迁移中最需要改造的部分。
Linaria的基本写法
先安装依赖:
npm install @linaria/core @linaria/react @linaria/babel-preset
组件写法和styled-components很像,上手成本不高:
import { styled } from '@linaria/react';
export const Card = styled.div`
padding: 16px;
border-radius: 8px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
`;
如果只想加个类名,不想创建新组件,可以用css:
import { css } from '@linaria/core';
const title = css`
font-size: 20px;
font-weight: 600;
`;
function Header() {
return <h1 className={title}>标题</h1>;
}
还有一个很实用的cx函数,用来做类名组合,行为类似classnames库:
import { css, cx } from '@linaria/core';
const base = css`padding: 12px;`;
const primary = css`background: #1677ff; color: #fff;`;
function Button({ variant }) {
return (
<button className={cx(base, variant === 'primary' && primary)}>
按钮
</button>
);
}
迁移的具体步骤
第一步是改造构建配置。以Webpack为例,需要在Babel配置中加入Linaria的preset,并在CSS处理链路中让linaria/loader参与进来:
// babel.config.js
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@linaria'
]
};
// webpack.config.js 片段
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'linaria/loader',
options: { sourceMap: true }
}
]
}
]
}
如果项目用的是Vite,官方提供了@linaria/vite插件,接入更简单,把插件加进plugins数组即可。Next.js项目则建议使用@linaria/next-loader或者社区封装的配置方案,避免和自身的样式处理流程冲突。
第二步是逐组件改写样式。建议按页面或组件目录为单位推进,每改完一块就在浏览器里对照旧版页面核对样式。GenieJS里基于props的内联样式写法需要重点处理,下面单独展开。
第三步是删除GenieJS依赖并清理全局样式注入逻辑。确认所有组件迁移完成后,移除相关包和它的运行时初始化代码,构建产物会明显瘦身。
动态样式怎么处理
这是迁移的核心难点。GenieJS里可以直接在样式函数里读props,例如根据主题色生成背景。Linaria编译期无法知道运行时的props值,所以要换思路。
优先方案是把动态值拆成有限的几个变体,用属性选择器或类名组合表达:
const variants = {
primary: css`background: #1677ff;`,
danger: css`background: #ff4d4f;`
};
function Tag({ type }) {
return <span className={cx(base, variants[type])}>{type}</span>;
}
其次是把真正无法穷举的值(比如用户自定义颜色)通过CSS变量传递。Linaria支持往styled组件上挂任意props,在模板字符串里用var()接收:
const Swatch = styled.div`
width: 40px;
height: 40px;
background: var(--swatch-color, #ccc);
`;
// 使用时
<Swatch style={{ '--swatch-color': userColor }} />
这种写法既保留了零运行时特性,又满足了运行时动态需求,是被官方推荐的兜底方案。还有一点要注意:Linaria的模板字符串里可以插值,但插值只能是css结果或编译期常量,不能是任意表达式,遇到报错时优先检查这一项。
迁移中容易踩的坑
第一个坑是选择器优先级变化。GenieJS生成的类名规则和Linaria不同,原本依赖特定顺序覆盖的样式可能失效或反过来被覆盖。迁移后要重点检查全局样式与组件样式的叠加关系,必要时用&提高组件内选择器权重,而不是滥用!important。
第二个坑是SSR与样式提取。虽然Linaria本身对SSR友好,但如果项目里还残留GenieJS的服务端样式收集逻辑,两套机制可能互相干扰,表现为样式闪烁或重复注入。务必在服务端入口彻底移除旧方案的样式提取代码。
第三个坑是开发环境热更新。Linaria的样式变更依赖构建管线,某些配置组合下HMR表现不稳定,建议开启sourceMap方便定位,并确认loader顺序正确——linaria/loader必须在css-loader之前执行。
迁移效果如何验证
验证可以从三个维度入手。看产物:构建后应该能在输出目录找到独立的CSS文件,且JS包里不再包含GenieJS的运行时代码;用打包分析工具对比迁移前后的chunk体积,通常能减少几十KB甚至更多。看性能:用Lighthouse对比首屏FCP和TTI指标,关注样式相关的主线程耗时是否下降。看回归:对关键页面做截图对比,确保视觉表现与迁移前一致。
完成这些检查后,项目就真正做到了样式零运行时:浏览器拿到的是一份可以直接解析的静态CSS,组件渲染路径上没有任何样式计算。对于样式逻辑复杂度不高、但又想保留组件化样式写法的React项目来说,这个迁移投入产出比相当可观。
ReactLinaria零运行时CSS-in-JS修改时间:2026-09-03 04:18:46