CSS-in-JS 真的比传统 CSS 更香吗?一位 Java 老兵的前端性能实测
上周五晚上十一点,办公室只剩我和空调还在坚持。耳机里放着 Lo-fi Beats,屏幕上是第 17 次构建失败的日志——“Critical dependency: the request of a dependency is an expression”,熟悉的 webpack 报错。我叹了口气,这都怪我们那个新来的前端实习生,非要在企业后台系统里引入 styled-components。产品经理说要“现代化 UI”,结果上线前夜,首屏加载时间从 1.2s 飙到了 3.8s。
我是深圳某传统制造企业数字化转型团队的 Java 开发,说白了就是“老后端被迫搞全栈”。三年前公司开始搞中台战略,我们这群写 Spring Boot 的突然被要求会 React、懂 DevOps、甚至要优化 Lighthouse 分数。说实话,一开始我对 CSS-in-JS 这种“把样式写进 JS 里”的做法嗤之以鼻:CSS 不就该是 CSS 吗?干嘛要污染我的逻辑代码?
但现实很骨感。随着微前端架构落地,组件复用需求暴增,传统 CSS 的全局作用域问题越来越头疼。两个团队写的 .button 样式互相覆盖,测试同学天天提 bug:“这个按钮怎么在 A 页面是蓝色,在 B 页面变紫色了?”运维也抱怨静态资源体积膨胀,CDN 带宽费用蹭蹭涨。
于是,我决定认真研究下现代样式方案。GitHub 上搜了一圈,发现主流无非两类:传统 CSS(含预处理器) vs CSS-in-JS(如 styled-components, Emotion)。今天就结合我们线上项目的真实数据,聊聊这两派到底谁更适合传统企业的数字化转型场景。
为什么传统 CSS 在微前端时代“水土不服”?
先说结论:不是 CSS 本身不行,而是我们的使用方式太原始。
早期我们直接用 <link> 引入全局 CSS 文件,所有组件共享一个命名空间。为了规避冲突,前端同事发明了各种“土办法”:
- BEM 命名规范(
.product-card__title--highlight) - 手动加前缀(
.team-a-button) - 甚至用随机哈希(
.btn-3x9f2)
但这些方法治标不治本。尤其当多个子应用通过 qiankun 集成时,样式隔离成了噩梦。更别提动态主题切换这种需求——总不能让用户刷新页面吧?
另外,资源加载效率也是硬伤。传统 CSS 虽然能单独缓存,但首屏往往只用到 30% 的样式规则,其余 70% 属于“无效加载”。Lighthouse 总是抱怨 “Reduce unused CSS”,可我们又没法精准按需加载。
CSS-in-JS 的诱惑与陷阱
CSS-in-JS 的核心卖点就俩字:组件化。每个组件自带样式,天然作用域隔离,还能利用 JS 的动态能力实现运行时主题切换。Emotion 甚至支持 SSR 优化,号称“零运行时”。
听起来很美好,对吧?但实际落地时,坑比想象中多。
性能陷阱一:运行时开销
以 styled-components 为例,它在浏览器里会动态生成 <style> 标签。这意味着:
- 首次渲染需要额外 JS 解析和 DOM 操作
- 动态插入样式可能触发重排(reflow)
- 复杂组件树会导致样式表碎片化
我们在测试环境做了对比(基于 React 18 + Webpack 5):
| 方案 | 首屏 FCP (ms) | Bundle Size (gzip) | TTI (ms) |
|---|---|---|---|
| 传统 CSS (SCSS + CSS Modules) | 1120 | 42KB | 1350 |
| styled-components (v6) | 2380 | 68KB | 2760 |
| Emotion (with SSR) | 1450 | 51KB | 1680 |
注:测试设备为 MacBook Pro M1,模拟 3G 网络,页面包含 50+ 组件
可以看到,纯客户端 CSS-in-JS 对性能影响显著。虽然 Emotion 通过 SSR 缓解了部分问题,但 bundle size 依然偏高——毕竟多了个运行时库。
性能陷阱二:缓存失效
传统 CSS 文件可以长期缓存(hash 文件名),但 CSS-in-JS 的样式通常内联在 JS 中。一旦业务逻辑更新,整个 JS bundle 的 hash 改变,连带样式缓存也失效了。这对带宽敏感的企业用户(比如工厂车间用 4G 网络)简直是灾难。
我们的折中方案:CSS Modules + 构建时优化
经过几轮 AB 测试,我们最终没选极端方案,而是走了一条“混合路线”:
- 基础组件库:用 SCSS + CSS Modules,确保零运行时开销
- 动态主题/复杂交互:局部引入 Emotion,仅用于需要运行时计算的场景
- 关键优化:通过 Webpack 插件自动提取 Critical CSS
具体配置如下:
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const Critters = require('critters-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.module\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: { modules: true }
},
'sass-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
}),
// 自动提取首屏关键 CSS 内联到 HTML
new Critters({
preload: 'swap',
pruneSource: true
})
]
};
效果立竿见影:
- 首屏 FCP 降至 980ms(比纯 CSS 还快!)
- 静态资源缓存命中率提升至 92%
- 样式冲突 Bug 归零
最关键的是,Java 后端同事终于不用半夜被叫起来查“前端样式问题”了(运维曾误以为是 Nginx 配置错误)。
GitHub 上值得关注的资源
如果你也在纠结样式方案,这几个开源项目值得深挖:
- Vanilla Extract:TypeScript 优先的 CSS-in-JS,编译时生成静态 CSS,兼顾开发体验与性能
- Linaria:零运行时 CSS-in-JS,通过 Babel 提取样式到 CSS 文件
- UnoCSS:原子化 CSS 引擎,按需生成极小体积样式
我们最近就在试 Vanilla Extract,它的 recipe API 让主题切换变得异常优雅:
// styles/theme.css.ts
import { createTheme } from '@vanilla-extract/css';
export const [themeClass, vars] = createTheme({
color: {
brand: '#3a86ff',
background: '#f8f9fa'
},
space: {
md: '16px'
}
});
// Button.tsx
import * as styles from './Button.css';
export const Button = () => (
<button className={styles.button}>Click me</button>
);
编译后输出纯 CSS 文件,完全无运行时负担。对于追求极致性能的传统企业,这才是理想方案。
写在最后:技术选型要看场景,别盲目跟风
回头看这场样式方案之争,其实没有银弹。CSS-in-JS 在快速原型、内部工具等场景确实高效,但对性能敏感、用户网络环境复杂的 ToB 系统,传统 CSS + 现代工程化手段仍是更稳妥的选择。
作为 Java 开发,我常被前端同事吐槽“思想老旧”。但经历过双 11 大促的线上事故后,我坚信:稳定性 > 新潮度,可维护性 > 开发爽度。毕竟,当车间主任在 4G 网络下打开采购系统时,他不在乎你用了多么 fancy 的样式方案,只关心页面能不能三秒内出来。
所以啊,别被社区 hype 带节奏。打开 DevTools,跑个 Lighthouse,用真实数据说话。毕竟,代码跑在用户设备上,不是跑在 Twitter 时间线上。
(写完这篇,赶紧去修复那个实习生留下的 styled-components 内存泄漏…… 啊,周五的 Lo-fi 又响起来了。)

评论 0