能彻底移除运行时样式注入逻辑——linaria和vanilla-extract在构建期生成CSS,完全消除insertRule、style标签动态插入等主线程同步操作,避免拖慢React commit阶段、掉帧及强制同步布局问题。

直接用 linaria 或 vanilla-extract 替换 styled-components 或 @emotion/react,能彻底移除运行时样式注入逻辑——不是“优化”,是让那部分代码根本不出现在浏览器里。
为什么 insertRule 和 style 标签插入是性能瓶颈
每次组件渲染,styled-components 都要调用 sheet.insertRule()、做哈希计算、拼接字符串、查重、插入 <style></style> 标签。这些全在主线程同步执行,尤其在列表滚动或动画帧中,会直接拖慢 commit 阶段。Chrome DevTools 的 Performance 面板里能看到大量 insertRule 调用堆叠,Lighthouse 也会报“避免强制同步布局”。
常见错误现象:
- React DevTools 显示某组件 mount 耗时中,
commit占比异常高(>40%) - 移动端滑动时掉帧,但 CPU 占用率明显高于纯 DOM 操作场景
- 用
performance.mark()包住组件挂载,发现style相关子任务始终存在
linaria 的 Babel 插件配置要点
linaria 在 Babel 编译阶段静态分析 css 模板字面量,生成类名和对应 CSS 文件。它不依赖运行时,但对 JS 表达式有硬性限制。
立即学习“前端免费学习笔记(深入)”;
- 只支持编译期可求值的表达式:允许
css`color: ${theme.primary}`(theme.primary是常量对象),禁止css`color: ${props.color}`(props 是运行时变量) - 不能用动态 import 或变量拼接路径,比如
import(`./${mode}.css`)会导致构建失败 - 需确保 Webpack/Vite 正确处理生成的 CSS:Vite 要开
css.codeSplit: true;Webpack 要配MiniCssExtractPlugin,否则 CSS 会漏掉或内联进 JS - 调试时别依赖浏览器里找
.linaria-xxx类名——它只是开发时 fallback,生产环境走的是真实 CSS 文件
vanilla-extract 的 .css.ts 文件约束
vanilla-extract 要求样式必须写在 .css.ts 文件里,不能塞进 React 组件内部。它靠 TypeScript 类型系统做静态分析,生成的 CSS 是真正零 runtime 的产物。
- 不支持闭包、函数调用、任意 JS 表达式:例如
col这种变量若未在编译期确定值,会报类型错误 - 主题切换必须用
defineTheme+ 多份预生成 CSS,不能靠 runtime 切 class 或修改 CSS 变量 - 生成的 CSS 文件路径由
compilePackage或 Vite 插件控制,需检查打包后 output 目录是否含.css文件,且被 HTML 正确引用 - VS Code 中能获得完整的类型提示和自动补全,但代价是开发时不能“边写组件边写样式”
怎么确认 runtime 开销真没了
不能只看 bundle size 小了没——关键是浏览器里有没有还干那些事。
- 打开 Chrome DevTools → Performance → 录制一次交互 → 搜索
insertRule、createTextNode、injectStyles,结果应几乎为 0 - 在
styled-components/src/constructors/injectStyles.ts或@emotion/sheet/src/index.js的insert方法上打断点,确认它们不再被触发 - 对比前后帧:用
performance.mark('mount-start')和performance.mark('mount-end')包住组件挂载过程,观察 style 相关子任务是否从 Performance 面板的 commit 阶段消失
最易被忽略的一点:CSS 文件虽已提取,但如果没被正确加载(比如路径 404、<link> 被误删、HTTP/2 push 配置错误),浏览器会回退到 runtime 注入,此时所有优化都白做。


















