实现一个简易的CSS-in-JS工具
最新在处理项目antd4升级到antd5,里面有一个性能问题需要排查antd5里面使用的css in js。
之前虽然对于css in js有一些耳闻,但并没有在项目中使用过,对其原理也没有特别了解,因此需要先学习一下。本文将参考@ant-design/cssinjs的源码,实现一个非常简单的css in js工具,了解其核心原理和实现。
什么是 CSS-in-JS
CSS-in-JS 是一种使用 JavaScript 编写 CSS 的技术,它允许你在 JavaScript 中定义样式,然后动态注入到页面中。
传统方式 vs CSS-in-JS
// 传统 CSS
// styles.css
.button { color: red; }
// component.jsx
import './styles.css';
<button className="button">Click</button>
// CSS-in-JS
const styles = { color: 'red' };
<button className={css(styles)}>Click</button>核心优势
- 样式隔离 - 自动生成唯一类名,避免冲突
还记得那些年被 .button 类名支配的恐惧吗?你写了个 .button,同事也写了个 .button,结果样式互相覆盖,页面乱成一团。CSS-in-JS 会自动生成 .css-abc123 这样的唯一类名,再也不用担心类名冲突了。
动态样式 - 基于 props/state 动态计算
想根据用户等级显示不同颜色?想根据进度条数值改变宽度?直接在 JS 里写逻辑就行,不用再搞什么
className={isPremium ? 'gold' : 'silver'}这种繁琐的类名切换。按需加载 - 只加载使用的样式
传统 CSS 文件一股脑全加载,用户可能只看了首页,结果把整个网站的样式都下载了。CSS-in-JS 只在组件渲染时才注入样式,用不到的组件,样式也不会加载。
类型安全 - TypeScript 支持
写错了属性名?拼错了颜色值?TypeScript 直接给你报错,不用等到浏览器里才发现问题。而且还有智能提示,开发体验不错(不过现在大部分IDE对于CSS、Less等提示也是比较到位的,两者差别不大)。
主题切换 - 轻松实现多主题
想做个暗黑模式?想让用户自定义主题色?CSS-in-JS 可以直接通过 Context 传递主题变量,所有组件自动响应,不用写一堆 CSS 变量或者 class 切换逻辑。
核心劣势
当然,CSS-in-JS 也不是银弹,它也有不少问题:
运行时开销 - 性能是最大的痛点
传统 CSS 是静态的,浏览器解析一次就完事了。CSS-in-JS 需要在运行时解析样式、生成哈希、注入 DOM,这些都要消耗性能。尤其是首屏渲染,可能会明显变慢。这也是为什么 antd5 升级后有些项目会遇到性能问题。
包体积增加 - 要引入额外的库
styled-components 压缩后也有 15KB+,@emotion/react 也要 10KB+。对于追求极致性能的项目来说,这个体积不算小。而且这些库的代码也要执行,也会占用 JS 主线程时间。
调试困难 - 生成的类名不直观
打开 DevTools 看到的是
.css-1a2b3c4,完全不知道这是哪个组件的样式。虽然可以配置生成可读的类名,但生产环境一般都会关闭(为了减小体积)。相比之下,传统 CSS 的.header-nav-button一眼就知道是哪里的样式。SSR 复杂度 - 服务端渲染要额外处理
传统 CSS 直接引入就行,CSS-in-JS 需要在服务端收集样式、序列化、注入到 HTML,客户端还要 hydrate。稍有不慎就会出现样式闪烁、重复注入等问题。配置起来也比较繁琐。
学习成本 - 团队需要适应新的写法
习惯了写 CSS 的同学,突然要在 JS 里写样式,还要理解哈希、缓存、注入这些概念,学习曲线还是有的。而且不同的 CSS-in-JS 库 API 差异很大,换个库可能又要重新学。
工具链支持 - 不如传统 CSS 成熟
CSS 有 PostCSS、Sass、Less 这些成熟的工具链,还有各种 lint、format、压缩工具。CSS-in-JS 的工具链相对不够完善,比如样式提取、Critical CSS 生成等功能,实现起来都比较麻烦。
缓存策略 - 不能利用浏览器缓存
传统 CSS 文件可以设置长期缓存,用户第二次访问直接从缓存读取。CSS-in-JS 的样式是动态生成的,每次都要重新解析注入,无法享受浏览器缓存带来的性能提升。
核心概念
CSS-in-JS 的实现围绕以下 5 个核心概念:(实际上是@ant-design/cssinjs的几个核心概念,其他的css in js库侧重点各有差异,我还没有细研究)
样式对象(Style Object)
使用 JavaScript 对象表示 CSS,其中的key或者value,就可以通过纯粹的js props进行传递。
const styleObject = {
color: 'red', // CSS 属性
fontSize: 14, // 数字自动添加 px
backgroundColor: '#fff', // 驼峰命名
padding: '8px 16px', // 字符串值
// 嵌套选择器
'&:hover': {
color: 'blue'
},
// 子选择器
'& > span': {
fontWeight: 'bold'
}
};关键点:
- 使用驼峰命名(backgroundColor)而非连字符(background-color)
- 数字值自动添加单位
- 支持嵌套语法
样式解析(Style Parsing)
将 上面 的样式对象(也就是JavaScript 对象)转换为 CSS 字符串,之后可以通过style标签直接注入到页面上,之后对应选择器的样式就会生效。
// 输入
{ color: 'red', fontSize: 14 }
// 输出
".css-abc123{color:red;font-size:14px;}"核心步骤:
- 遍历对象属性
- 驼峰转连字符(fontSize → font-size)
- 数字添加单位(14 → 14px)
- 处理嵌套选择器
- 拼接成 CSS 字符串
动态注入(Dynamic Injection)
运行时将 CSS 字符串注入到 DOM:
// 创建 style 标签
const style = document.createElement('style');
style.innerHTML = '.css-abc123{color:red;}';
document.head.appendChild(style);关键点:
- 在
<head>中创建<style>标签 - 设置唯一标识避免重复
- 支持更新和删除
哈希生成(Hash Generation)
为样式生成唯一标识:
// 相同样式生成相同哈希
hash({ color: 'red' }) // 'abc123'
hash({ color: 'red' }) // 'abc123'
// 不同样式生成不同哈希
hash({ color: 'blue' }) // 'def456'作用:
- 生成唯一类名(css-abc123)
- 实现样式缓存
- 避免样式冲突
样式缓存(Style Cache)
避免重复解析和注入:
const cache = new Map();
function css(styleObj) {
const hash = generateHash(styleObj);
if (cache.has(hash)) {
return cache.get(hash); // 缓存命中
}
const className = createStyle(styleObj);
cache.set(hash, className);
return className;
}优势:
- 相同样式只解析一次
- 相同样式只注入一次
- 显著提升性能
实现原理
整体流程
JavaScript 对象
↓
生成哈希(检查缓存)
↓
解析为 CSS 字符串
↓
注入到 DOM
↓
返回类名样式解析器
核心功能: 将对象转换为 CSS 字符串
// 驼峰转连字符
function camelToKebab(str: string): string {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
}
// 格式化值(添加单位)
function formatValue(property: string, value: any): string {
// 不需要单位的属性
const unitless = ['opacity', 'zIndex', 'fontWeight', 'lineHeight'];
if (typeof value === 'number' && !unitless.includes(property)) {
return `${value}px`;
}
return String(value);
}
// 处理选择器
function processSelector(selector: string, parent: string): string {
if (selector.includes('&')) {
return selector.replace(/&/g, parent); // & → .parent
}
if (selector.startsWith(':')) {
return `${parent}${selector}`; // :hover → .parent:hover
}
return `${parent} ${selector}`; // .child → .parent .child
}
// 解析样式
function parseStyle(styles: any, selector: string): string {
let css = '';
const basic: string[] = [];
const nested: Array<[string, any]> = [];
// 分离基础样式和嵌套样式
for (const [key, value] of Object.entries(styles)) {
if (typeof value === 'object') {
nested.push([key, value]);
} else {
const prop = camelToKebab(key);
const val = formatValue(key, value);
basic.push(`${prop}:${val};`);
}
}
// 生成基础样式
if (basic.length > 0) {
css += `${selector}{${basic.join('')}}`;
}
// 递归处理嵌套
for (const [nestedSel, nestedStyles] of nested) {
const fullSelector = processSelector(nestedSel, selector);
css += parseStyle(nestedStyles, fullSelector);
}
return css;
}示例:
const styles = {
color: 'red',
fontSize: 14,
'&:hover': {
color: 'blue'
}
};
parseStyle(styles, '.button');
// 输出:
// .button{color:red;font-size:14px;}
// .button:hover{color:blue;}哈希生成器
核心功能: 为样式对象生成唯一标识,本质上可以理解为某种摘要算法,将相同内容的样式对象计算出一个相同的hash。
// 简单哈希算法(djb2)
function simpleHash(str: string): string {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = (hash * 33) ^ str.charCodeAt(i);
}
return (hash >>> 0).toString(36);
}
// 对象序列化(保证顺序一致)
function serialize(obj: any): string {
if (typeof obj !== 'object' || obj === null) {
return String(obj);
}
if (Array.isArray(obj)) {
return `[${obj.map(serialize).join(',')}]`;
}
// 对象按键排序
const keys = Object.keys(obj).sort();
const pairs = keys.map(k => `${k}:${serialize(obj[k])}`);
return `{${pairs.join(',')}}`;
}
// 生成哈希
function generateHash(styles: any): string {
return simpleHash(serialize(styles));
}示例:
generateHash({ color: 'red', fontSize: 14 });
// 输出: 'abc123'
generateHash({ fontSize: 14, color: 'red' });
// 输出: 'abc123' // 顺序不同,哈希相同DOM 注入器
核心功能: 动态创建和管理 style 标签
// 查找已存在的 style 标签
function findStyle(id: string): HTMLStyleElement | null {
return document.querySelector(`style[data-css-id="${id}"]`);
}
// 注入样式
function injectStyle(css: string, id: string): HTMLStyleElement {
let style = findStyle(id);
if (style) {
// 已存在,检查内容是否相同
if (style.innerHTML !== css) {
style.innerHTML = css; // 更新
}
return style;
}
// 不存在,创建新的
style = document.createElement('style');
style.setAttribute('data-css-id', id);
style.innerHTML = css;
document.head.appendChild(style);
return style;
}
// 删除样式
function removeStyle(id: string): void {
const style = findStyle(id);
if (style) {
style.remove();
}
}特性:
- 避免重复创建
- 支持样式更新
- 支持样式删除
- 使用 data 属性标识
缓存系统
核心功能: 管理样式缓存和引用计数
interface CacheEntry {
hash: string;
className: string;
css: string;
refCount: number; // 引用计数
}
class StyleCache {
private cache = new Map<string, CacheEntry>();
get(hash: string): CacheEntry | undefined {
return this.cache.get(hash);
}
set(hash: string, entry: CacheEntry): void {
this.cache.set(hash, entry);
}
has(hash: string): boolean {
return this.cache.has(hash);
}
// 增加引用
addRef(hash: string): void {
const entry = this.cache.get(hash);
if (entry) entry.refCount++;
}
// 减少引用
removeRef(hash: string): void {
const entry = this.cache.get(hash);
if (entry) {
entry.refCount--;
if (entry.refCount <= 0) {
this.cache.delete(hash);
removeStyle(hash);
}
}
}
}
const globalCache = new StyleCache();优化策略:
- 引用计数:组件卸载时自动清理
- WeakMap:避免内存泄漏
- LRU 策略:限制缓存大小
核心 API
核心功能: 整合所有模块
// 主函数:创建样式并返回类名
function css(styles: any): string {
// 1. 生成哈希
const hash = generateHash(styles);
// 2. 检查缓存
if (globalCache.has(hash)) {
const entry = globalCache.get(hash)!;
globalCache.addRef(hash);
return entry.className;
}
// 3. 生成类名
const className = `css-${hash}`;
// 4. 解析样式
const cssString = parseStyle(styles, `.${className}`);
// 5. 注入 DOM
injectStyle(cssString, hash);
// 6. 存入缓存
globalCache.set(hash, {
hash,
className,
css: cssString,
refCount: 1
});
return className;
}React 集成
核心功能: 提供 React Hook
import { useEffect, useMemo, useRef } from 'react';
function useStyles<T extends Record<string, any>>(
styles: T
): { [K in keyof T]: string } {
const prevHashesRef = useRef<string[]>([]);
// 生成类名
const classNames = useMemo(() => {
const result: any = {};
const hashes: string[] = [];
for (const key in styles) {
result[key] = css(styles[key]);
hashes.push(generateHash(styles[key]));
}
prevHashesRef.current = hashes;
return result;
}, [styles]);
// 清理
useEffect(() => {
return () => {
prevHashesRef.current.forEach(hash => {
globalCache.removeRef(hash);
});
};
}, []);
return classNames;
}
// 辅助函数:合并类名
function cx(...names: (string | undefined | false | null)[]): string {
return names.filter(Boolean).join(' ');
}完整示例
基础使用
import { useStyles } from './mini-cssinjs';
function Button() {
const styles = useStyles({
button: {
padding: '8px 16px',
fontSize: 14,
backgroundColor: '#1890ff',
color: '#fff',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
transition: 'all 0.3s',
'&:hover': {
backgroundColor: '#40a9ff'
},
'&:active': {
backgroundColor: '#096dd9'
}
}
});
return <button className={styles.button}>Click Me</button>;
}动态样式
function Button({ type = 'default' }: { type?: 'primary' | 'default' }) {
const colors = {
primary: { bg: '#1890ff', hover: '#40a9ff' },
default: { bg: '#fff', hover: '#f0f0f0' }
};
const styles = useStyles({
button: {
padding: '8px 16px',
backgroundColor: colors[type].bg,
color: type === 'primary' ? '#fff' : '#000',
border: type === 'default' ? '1px solid #d9d9d9' : 'none',
'&:hover': {
backgroundColor: colors[type].hover
}
}
});
return <button className={styles.button}>Click Me</button>;
}复杂组件
function Card() {
const styles = useStyles({
card: {
padding: 24,
backgroundColor: '#fff',
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
'&:hover': {
boxShadow: '0 4px 16px rgba(0,0,0,0.15)',
transform: 'translateY(-2px)'
}
},
header: {
display: 'flex',
justifyContent: 'space-between',
marginBottom: 16,
paddingBottom: 16,
borderBottom: '1px solid #f0f0f0'
},
title: {
fontSize: 18,
fontWeight: 'bold',
margin: 0
},
content: {
fontSize: 14,
lineHeight: 1.6,
color: '#666'
}
});
return (
<div className={styles.card}>
<div className={styles.header}>
<h3 className={styles.title}>Card Title</h3>
</div>
<div className={styles.content}>
Card content...
</div>
</div>
);
}性能优化
缓存策略,避免生成相同的样式
问题: 相同样式重复解析和注入
解决: 使用哈希缓存
// ❌ 没有缓存 - 每次都解析
function Button() {
const className = css({ color: 'red' }); // 解析 + 注入
return <button className={className}>Click</button>;
}
// 渲染 100 个按钮 = 100 次解析 + 100 次注入
// ✅ 有缓存 - 只解析一次
function Button() {
const className = css({ color: 'red' }); // 第一次:解析 + 注入
return <button className={className}>Click</button>; // 后续:缓存命中
}
// 渲染 100 个按钮 = 1 次解析 + 1 次注入对象引用优化,避免缓存失效
引入了缓存策略,随之而来的问题是哈希缓存算法需要尽量保证键值一致的对象,可以生成相同的hash,如果是基于对象引用来生成的hash,就很有可能导致缓存失效。
这是最常见的问题,也是最容易被忽略的。
// ❌ 性能杀手 - 每次渲染都是新对象
function UserCard({ user }) {
const styles = useStyles({
card: {
backgroundColor: user.isPremium ? '#gold' : '#silver' // 每次都是新对象!
}
});
return <div className={styles.card}>{user.name}</div>;
}
// 渲染 1000 个用户 = 1000 次样式解析 + 1000 个 style 标签为什么会慢?
- 每次渲染都生成新的样式对象
- 缓存完全失效
- 大量重复的 CSS 被注入到 DOM
正确做法:
// ✅ 方案 1:提前定义样式
const premiumStyle = { card: { backgroundColor: '#gold' } };
const normalStyle = { card: { backgroundColor: '#silver' } };
function UserCard({ user }) {
const styles = useStyles(user.isPremium ? premiumStyle : normalStyle);
return <div className={styles.card}>{user.name}</div>;
}
// ✅ 方案 2:使用 CSS 变量
function UserCard({ user }) {
const styles = useStyles({
card: {
backgroundColor: 'var(--card-bg)'
}
});
return (
<div
className={styles.card}
style={{ '--card-bg': user.isPremium ? '#gold' : '#silver' }}
>
{user.name}
</div>
);
}引用计数在组件卸载后移除样式标签
这在传统的css文件方案中并不太容易实现。
问题: 组件卸载后样式残留
这个问题不会立即显现,但时间长了会导致内存泄漏。
// ❌ 样式一直堆积
function Modal({ visible }) {
const styles = useStyles({
modal: { /* ... */ }
});
if (!visible) return null;
return <div className={styles.modal}>Modal Content</div>;
}
// 打开关闭 100 次 = 100 个 style 标签残留在 DOM 里为什么会有问题?
- 组件卸载了,但 style 标签还在
- 页面上的 style 标签越来越多
- 最终导致页面变慢、内存占用高
正确做法:
前面实现的引用计数机制就是为了解决这个问题。确保你的 CSS-in-JS 库支持自动清理,或者手动管理样式的生命周期。
解决: 引用计数自动清理
function useStyles(styles) {
const hashes = useRef([]);
// 组件挂载:增加引用
const classNames = useMemo(() => {
const result = {};
hashes.current = [];
for (const key in styles) {
const hash = generateHash(styles[key]);
result[key] = css(styles[key]);
hashes.current.push(hash);
globalCache.addRef(hash); // +1
}
return result;
}, [styles]);
// 组件卸载:减少引用
useEffect(() => {
return () => {
hashes.current.forEach(hash => {
globalCache.removeRef(hash); // -1
});
};
}, []);
return classNames;
}批量操作,优化DOM操作
问题: 频繁的 DOM 操作导致性能下降
解决: 使用 DocumentFragment 批量插入
function batchInjectStyles(styles: Array<{ css: string; id: string }>) {
const fragment = document.createDocumentFragment();
styles.forEach(({ css, id }) => {
if (!findStyle(id)) {
const style = document.createElement('style');
style.setAttribute('data-css-id', id);
style.innerHTML = css;
fragment.appendChild(style);
}
});
document.head.appendChild(fragment); // 一次性插入
}小心在循环中使用动态样式
这个问题在列表渲染时特别常见。
// ❌ 灾难级性能 - 每个列表项都生成独立样式
function TodoList({ todos }) {
return todos.map(todo => {
const styles = useStyles({
item: {
color: todo.completed ? '#999' : '#000', // 每个 todo 都不一样!
textDecoration: todo.completed ? 'line-through' : 'none'
}
});
return <div className={styles.item}>{todo.text}</div>;
});
}
// 100 个 todo = 100 个不同的样式 = 页面卡成 PPT为什么会慢?
- 每个列表项的样式都略有不同
- 无法复用缓存
- DOM 中充斥着大量相似的 style 标签
正确做法:
// ✅ 使用类名切换
const todoStyles = {
item: { color: '#000' },
completed: {
color: '#999',
textDecoration: 'line-through'
}
};
function TodoList({ todos }) {
const styles = useStyles(todoStyles);
return todos.map(todo => (
<div className={cx(styles.item, todo.completed && styles.completed)}>
{todo.text}
</div>
));
}
// 100 个 todo = 只有 2 个样式类 = 丝滑流畅避免在 render 函数里做复杂计算
有时候为了"灵活",会在样式里做一些计算,结果把性能拖垮了。
// ❌ 每次渲染都要算一遍
function ProgressBar({ progress }) {
const styles = useStyles({
bar: {
width: `${progress}%`,
// 根据进度计算颜色
backgroundColor: progress < 30 ? 'red'
: progress < 70 ? 'orange'
: 'green',
// 还要算阴影
boxShadow: `0 0 ${progress / 10}px rgba(0,0,0,0.3)`
}
});
return <div className={styles.bar} />;
}为什么会慢?
- 每个不同的 progress 值都生成新样式
- progress 从 0 到 100,可能生成 100 个样式
- 计算本身也消耗性能
正确做法:
// ✅ 用 CSS 变量 + 预定义样式
const progressStyles = {
bar: {
width: 'var(--progress)',
backgroundColor: 'var(--color)',
boxShadow: 'var(--shadow)'
}
};
function ProgressBar({ progress }) {
const styles = useStyles(progressStyles);
const color = progress < 30 ? 'red' : progress < 70 ? 'orange' : 'green';
return (
<div
className={styles.bar}
style={{
'--progress': `${progress}%`,
'--color': color,
'--shadow': `0 0 ${progress / 10}px rgba(0,0,0,0.3)`
}}
/>
);
}
// 只生成 1 个样式类,无论 progress 是多少SSR 时样式重复注入
服务端渲染时,如果处理不当,会导致样式在客户端重复注入。
// ❌ 服务端生成了样式,客户端又生成一遍
// 服务端 HTML:
<style>.css-abc { color: red; }</style>
// 客户端 hydrate 后:
<style>.css-abc { color: red; }</style> <!-- 服务端的 -->
<style>.css-abc { color: red; }</style> <!-- 客户端又生成了一个! -->正确做法:
// 服务端:收集样式
const cache = createCache();
const html = renderToString(
<CacheProvider value={cache}>
<App />
</CacheProvider>
);
const styles = extractStyles(cache);
// 客户端:复用服务端样式
const cache = createCache();
hydrateCache(cache, window.__STYLES__); // 标记已存在的样式
hydrate(
<CacheProvider value={cache}>
<App />
</CacheProvider>,
document.getElementById('root')
);WeakMap 优化
问题: 对象缓存导致内存泄漏
解决: 使用 WeakMap 自动回收
// 对象引用缓存
const objectCache = new WeakMap<object, string>();
function generateHash(styles: any): string {
// 如果是对象且已缓存,直接返回
if (typeof styles === 'object' && objectCache.has(styles)) {
return objectCache.get(styles)!;
}
const hash = simpleHash(serialize(styles));
// 缓存对象引用
if (typeof styles === 'object') {
objectCache.set(styles, hash);
}
return hash;
}再看运行时css in js的优劣
前面我们实现了一个简易的 CSS-in-JS 工具,也列举了它的优劣势。但在实际项目中,尤其是 React 18 这种对性能有更高要求的环境下,运行时 CSS-in-JS 的问题会被进一步放大。这里我们深入聊聊它的灵活性和性能之间的权衡。
运行时的灵活性:双刃剑
运行时 CSS-in-JS 最大的卖点就是"灵活"——你可以在运行时根据任何 JavaScript 变量动态生成样式。这听起来很美好,但也是性能问题的根源。
灵活性的诱惑
// 看起来很爽的写法
function DynamicButton({ theme, size, status, priority }) {
const styles = useStyles({
button: {
// 根据主题动态计算
backgroundColor: theme.colors[priority],
color: theme.textColors[priority],
// 根据尺寸动态计算
padding: size === 'large' ? '12px 24px' : '8px 16px',
fontSize: size === 'large' ? 16 : 14,
// 根据状态动态计算
opacity: status === 'disabled' ? 0.5 : 1,
cursor: status === 'disabled' ? 'not-allowed' : 'pointer',
// 还可以做复杂计算
boxShadow: `0 ${priority * 2}px ${priority * 4}px rgba(0,0,0,0.${priority})`,
}
});
return <button className={styles.button}>Click</button>;
}这种写法确实很灵活,但问题在于:
每个不同的 props 组合都会生成新样式
- 3 个 priority × 2 个 size × 2 个 status = 12 种样式
- 如果 theme 也是动态的,组合数会爆炸式增长
缓存基本失效
- 即使两个按钮的样式看起来一样,但因为对象引用不同,还是会生成新的样式
- 缓存命中率极低
DOM 中充斥着大量 style 标签
- 页面上可能有几十个按钮,每个都有自己的 style 标签
- 浏览器要解析和应用这些样式,性能直线下降
在 React 18 中的问题
React 18 引入了并发渲染(Concurrent Rendering),这让运行时 CSS-in-JS 的问题更加严重。
问题 1:样式注入阻塞渲染
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent /> {/* 包含大量 CSS-in-JS 组件 */}
</Suspense>
);
}在 React 18 的并发模式下:
- React 可以中断渲染,优先处理更重要的更新
- 但 CSS-in-JS 的样式注入是同步的,无法被中断
- 大量样式注入会阻塞主线程,导致页面卡顿
问题 2:与 Transition API 的冲突
function SearchPage() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
startTransition(() => {
setQuery(value); // 低优先级更新
});
};
return (
<>
<SearchInput onChange={handleSearch} />
<SearchResults query={query} /> {/* 包含大量动态样式 */}
</>
);
}理想情况下,startTransition 应该让搜索结果的渲染不阻塞输入。但如果 SearchResults 里有大量运行时 CSS-in-JS:
- 样式解析和注入是同步的,无法被标记为低优先级
- 用户输入还是会被阻塞
- Transition API 的优势完全发挥不出来
问题 3:Streaming SSR 的性能瓶颈
React 18 的 Streaming SSR 可以让页面分块渲染,但运行时 CSS-in-JS 会破坏这个优势:
// 服务端
<Suspense fallback={<Skeleton />}>
<SlowComponent /> {/* 包含大量 CSS-in-JS */}
</Suspense>- 传统 CSS:HTML 可以立即流式传输,CSS 文件并行加载
- 运行时 CSS-in-JS:必须等组件渲染完,收集所有样式,才能发送 HTML
- 首字节时间(TTFB)显著增加
useInsertionEffect
useInsertionEffect 是 React 18 为 CSS-in-JS 提供的性能优化方案:
- 执行时机更早 - 在 DOM 变更前注入样式
- 避免双重布局 - 浏览器只需要计算一次样式
- 消除样式闪烁 - 样式在 DOM 渲染前就已经准备好
- 提升 40% 性能 - 相比
useLayoutEffect
但它也有局限性:
- 只能用于 CSS-in-JS - 不要用它做其他事情
- 不能访问 DOM - 执行时 DOM 还没创建
- 不能使用 setState - 会导致额外的渲染
为什么需要 useInsertionEffect?
在 React 18 之前,CSS-in-JS 库通常使用 useLayoutEffect 来注入样式:
function useStyles(styles) {
const [className, setClassName] = useState('');
useLayoutEffect(() => {
// 在这里注入样式
const hash = generateHash(styles);
const css = parseStyle(styles, `.css-${hash}`);
injectStyle(css, hash);
setClassName(`css-${hash}`);
}, [styles]);
return className;
}这种方式有个严重的问题:
1. React 渲染组件(读取 DOM 布局)
2. useLayoutEffect 执行(注入样式,修改 DOM)
3. 浏览器重新计算样式
4. React 再次读取 DOM 布局(因为样式变了)
5. 浏览器重新绘制这导致了双重布局计算,性能很差。而且在并发模式下,可能会出现样式闪烁:
function Button() {
const styles = useStyles({ button: { color: 'red' } });
// 第一次渲染:className 还是空的,按钮没有样式
// useLayoutEffect 执行后:样式注入,按钮突然有了样式
// 用户可能会看到一瞬间的无样式内容(FOUC)
return <button className={styles.button}>Click</button>;
}useInsertionEffect 的执行时机
useInsertionEffect 的执行时机比 useLayoutEffect 更早:
1. React 渲染组件(生成虚拟 DOM)
2. useInsertionEffect 执行(注入样式)← 在这里!
3. React 提交到真实 DOM
4. 浏览器计算样式和布局(只需要一次)
5. useLayoutEffect 执行
6. 浏览器绘制关键点:
- 不能使用 setState -
useInsertionEffect执行时,DOM 还没有更新,调用 setState 会导致额外的渲染 - 使用 ref 存储结果 - 因为不能用 state,所以用 ref 来存储类名
- 只做 DOM 插入 - 这个 Hook 的设计目的就是插入
<style>标签,不要做其他事情
注意意事项
只在 CSS-in-JS 库中使用
useInsertionEffect是专门为 CSS-in-JS 设计的,不要用它做其他事情。React 官方文档明确说明:useInsertionEffect is for CSS-in-JS library authors. Unless you are working on a CSS-in-JS library and need a place to inject the styles, you probably want useEffect or useLayoutEffect instead.
不能访问 refs
typescriptfunction MyComponent() { const ref = useRef(null); useInsertionEffect(() => { console.log(ref.current); // ❌ null!DOM 还没创建 }); return <div ref={ref}>Hello</div>; }不能读取 DOM 布局
typescriptuseInsertionEffect(() => { const width = document.body.offsetWidth; // ❌ 可能触发强制同步布局 });服务端渲染时不执行
和
useEffect、useLayoutEffect一样,useInsertionEffect只在客户端执行。
性能问题的本质
运行时 CSS-in-JS 的性能问题,本质上是把构建时的工作推迟到了运行时。
传统 CSS 的工作流
开发时写 CSS → 构建时处理(压缩、autoprefixer)→ 浏览器加载静态文件 → 解析应用- 大部分工作在构建时完成
- 浏览器只需要解析和应用
- 可以利用 HTTP 缓存
运行时 CSS-in-JS 的工作流
开发时写 JS 对象 → 浏览器加载 JS → 运行时解析对象 → 生成哈希 → 转换为 CSS → 注入 DOM → 浏览器解析应用- 大部分工作在运行时完成
- 占用 JS 主线程时间
- 每次访问都要重新执行
- 无法利用 HTTP 缓存
具体的性能开销
以一个中等规模的页面为例(100 个组件,每个组件 3-5 个样式规则):
// 运行时开销分解
1. 对象序列化:100 个组件 × 4 个样式 × 0.1ms = 40ms
2. 哈希计算:400 个样式 × 0.05ms = 20ms
3. CSS 字符串生成:400 个样式 × 0.2ms = 80ms
4. DOM 注入:400 个 style 标签 × 0.1ms = 40ms
5. 浏览器重新计算样式:~50ms
总计:~230ms这还是理想情况(缓存命中率高)。如果缓存命中率低,开销会翻倍。
对比传统 CSS:
1. 加载 CSS 文件:~20ms(gzip 压缩后通常很小)
2. 浏览器解析:~30ms
总计:~50ms差距接近 5 倍。
替代方案
如果你遇到了运行时 CSS-in-JS 的性能问题,可以考虑这些方案:
CSS Modules + CSS 变量
// Button.module.css
.button {
padding: 8px 16px;
background-color: var(--button-bg);
color: var(--button-color);
}
// Button.tsx
import styles from './Button.module.css';
function Button({ type }) {
const vars = {
'--button-bg': type === 'primary' ? 'blue' : 'white',
'--button-color': type === 'primary' ? 'white' : 'black',
};
return <button className={styles.button} style={vars}>Click</button>;
}- 样式隔离
- 动态主题
- 性能接近传统 CSS
零运行时 CSS-in-JS
使用编译时 CSS-in-JS,如 Vanilla Extract、Linaria:
// styles.css.ts(编译时处理)
import { style } from '@vanilla-extract/css';
export const button = style({
padding: '8px 16px',
backgroundColor: 'blue',
});
// Button.tsx
import * as styles from './styles.css';
function Button() {
return <button className={styles.button}>Click</button>;
}- 编译时生成静态 CSS 文件
- 运行时零开销
- 保留类型安全
这些方案本质上跟写css module或者css文件的差别不大,最后都是导出了独立的css静态文件。
Tailwind CSS
function Button({ type }) {
return (
<button className={cn(
'px-4 py-2 rounded',
type === 'primary' ? 'bg-blue-500 text-white' : 'bg-white text-black'
)}>
Click
</button>
);
}- 原子化 CSS
- 按需生成
- 性能优秀
总结
CSS-in-JS 的实现围绕 5 个核心概念:
- 样式对象 - 使用 JavaScript 对象表示 CSS
- 样式解析 - 将对象转换为 CSS 字符串
- 哈希生成 - 为样式生成唯一标识
- 动态注入 - 运行时创建 style 标签
- 样式缓存 - 避免重复计算和注入
需要实现的要点包括
- 解析器: 驼峰转连字符 + 自动添加单位 + 处理嵌套
- 哈希: 对象序列化 + 简单哈希算法
- 注入: 创建 style 标签 + 避免重复
- 缓存: Map 存储 + 引用计数 + 自动清理
- React: Hook 封装 + 生命周期管理
运行时 CSS-in-JS 的灵活性是把双刃剑:
优势:
- 极致的动态能力
- 完美的样式隔离
- 优秀的开发体验
代价:
- 显著的运行时开销
- 与 React 18 新特性的冲突
- 难以优化的性能瓶颈
在 React 18 及未来的 React 版本中,运行时 CSS-in-JS 的性能问题会越来越明显。如果你的项目对性能有要求
@ant-design/cssinjs里面对于cssinjs的性能优化关键
- 缓存命中率 - 使用稳定的对象引用
- 引用计数 - 自动清理未使用的样式
- 批量操作 - 减少 DOM 操作次数
- WeakMap - 避免内存泄漏
但实际上,由于css in js 本身运行时存在的性能问题是无法被根治的,在antd6中,整个antd的样式切换为了0运行时的方案。
接下来可以更有针对性的学习@ant-design/cssinjs的源码
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。
