Skip to content

Commit 230036c

Browse files
committed
fix(tasty): injection optimizations
1 parent e3c6fae commit 230036c

File tree

3 files changed

+97
-28
lines changed

3 files changed

+97
-28
lines changed

src/tasty/injector/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ function getGlobalInjector(): StyleInjector {
9191
return storage[GLOBAL_INJECTOR_KEY]!;
9292
}
9393

94+
/**
95+
* Allocate a className for a cacheKey without injecting styles yet
96+
*/
97+
export function allocateClassName(
98+
cacheKey: string,
99+
options?: { root?: Document | ShadowRoot },
100+
): { className: string; isNewAllocation: boolean } {
101+
return getGlobalInjector().allocateClassName(cacheKey, options);
102+
}
103+
94104
/**
95105
* Inject styles and return className with dispose function
96106
*/

src/tasty/injector/injector.ts

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,46 @@ export class StyleInjector {
3939
this.sheetManager = new SheetManager(config);
4040
}
4141

42+
/**
43+
* Allocate a className for a cacheKey without injecting styles yet.
44+
* This allows separating className allocation (render phase) from style injection (insertion phase).
45+
*/
46+
allocateClassName(
47+
cacheKey: string,
48+
options?: { root?: Document | ShadowRoot },
49+
): { className: string; isNewAllocation: boolean } {
50+
const root = options?.root || document;
51+
const registry = this.sheetManager.getRegistry(root);
52+
53+
// Check if we can reuse existing className for this cache key
54+
if (registry.rules.has(cacheKey)) {
55+
const existingRuleInfo = registry.rules.get(cacheKey)!;
56+
return {
57+
className: existingRuleInfo.className,
58+
isNewAllocation: false,
59+
};
60+
}
61+
62+
// Generate new className and reserve it
63+
const className = generateClassName(registry.classCounter++);
64+
65+
// Create placeholder RuleInfo to reserve the className
66+
const placeholderRuleInfo = {
67+
className,
68+
ruleIndex: -1, // Placeholder - will be set during actual injection
69+
sheetIndex: -1, // Placeholder - will be set during actual injection
70+
};
71+
72+
// Reserve both className and cacheKey mappings
73+
registry.rules.set(className, placeholderRuleInfo);
74+
registry.rules.set(cacheKey, placeholderRuleInfo);
75+
76+
return {
77+
className,
78+
isNewAllocation: true,
79+
};
80+
}
81+
4282
/**
4383
* Inject styles from StyleResult objects
4484
*/
@@ -61,27 +101,36 @@ export class StyleInjector {
61101
// Check if we can reuse based on cache key
62102
const cacheKey = options?.cacheKey;
63103
let className: string;
104+
let isPreAllocated = false;
64105

65106
if (cacheKey && registry.rules.has(cacheKey)) {
66107
// Reuse existing class for this cache key
67108
const existingRuleInfo = registry.rules.get(cacheKey)!;
68109
className = existingRuleInfo.className;
69-
const currentRefCount = registry.refCounts.get(className) || 0;
70-
registry.refCounts.set(className, currentRefCount + 1);
71110

72-
// Update metrics
73-
if (registry.metrics) {
74-
registry.metrics.hits++;
75-
}
111+
// Check if this is a placeholder (pre-allocated but not yet injected)
112+
isPreAllocated =
113+
existingRuleInfo.ruleIndex === -1 && existingRuleInfo.sheetIndex === -1;
76114

77-
return {
78-
className,
79-
dispose: () => this.dispose(className, registry),
80-
};
81-
}
115+
if (!isPreAllocated) {
116+
// Already injected - just increment refCount
117+
const currentRefCount = registry.refCounts.get(className) || 0;
118+
registry.refCounts.set(className, currentRefCount + 1);
82119

83-
// Generate new className
84-
className = generateClassName(registry.classCounter++);
120+
// Update metrics
121+
if (registry.metrics) {
122+
registry.metrics.hits++;
123+
}
124+
125+
return {
126+
className,
127+
dispose: () => this.dispose(className, registry),
128+
};
129+
}
130+
} else {
131+
// Generate new className
132+
className = generateClassName(registry.classCounter++);
133+
}
85134

86135
// Process rules: handle needsClassName flag and apply specificity
87136
const rulesToInsert = rules.map((rule) => {
@@ -147,11 +196,19 @@ export class StyleInjector {
147196

148197
// Store in registry
149198
registry.refCounts.set(className, 1);
150-
registry.rules.set(className, ruleInfo);
151199

152-
// Also store by cache key if provided
153-
if (cacheKey) {
154-
registry.rules.set(cacheKey, ruleInfo);
200+
if (isPreAllocated) {
201+
// Update the existing placeholder entries with real rule info
202+
registry.rules.set(className, ruleInfo);
203+
if (cacheKey) {
204+
registry.rules.set(cacheKey, ruleInfo);
205+
}
206+
} else {
207+
// Store new entries
208+
registry.rules.set(className, ruleInfo);
209+
if (cacheKey) {
210+
registry.rules.set(cacheKey, ruleInfo);
211+
}
155212
}
156213

157214
// Update metrics

src/tasty/tasty.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from 'react';
1414
import { isValidElementType } from 'react-is';
1515

16-
import { inject, injectGlobal } from './injector';
16+
import { allocateClassName, inject, injectGlobal } from './injector';
1717
import { BreakpointsContext } from './providers/BreakpointsProvider';
1818
import { BASE_STYLES } from './styles/list';
1919
import { Styles, StylesInterface } from './styles/types';
@@ -382,20 +382,20 @@ function tastyElement<K extends StyleList, V extends VariantMap>(
382382

383383
const disposeRef = useRef<(() => void) | null>(null);
384384

385-
// Single injection pattern - inject once and get both className and dispose
386-
const injectionResult = useMemo(() => {
387-
if (!directResult.rules.length) return null;
388-
return inject(directResult.rules, { cacheKey });
389-
}, [directResult.rules, cacheKey]);
390-
391-
const injectedClassName = injectionResult?.className || '';
385+
// Allocate className in render phase (safe for React Strict Mode)
386+
const allocatedClassName = useMemo(() => {
387+
if (!directResult.rules.length || !cacheKey) return '';
388+
const { className } = allocateClassName(cacheKey);
389+
return className;
390+
}, [directResult.rules.length, cacheKey]);
392391

393-
// Handle disposal in useInsertionEffect
392+
// Inject styles in insertion effect (avoids render phase side effects)
394393
useInsertionEffect(() => {
395394
// Cleanup previous disposal reference
396395
disposeRef.current?.();
397396

398-
if (injectionResult) {
397+
if (directResult.rules.length > 0) {
398+
const injectionResult = inject(directResult.rules, { cacheKey });
399399
disposeRef.current = injectionResult.dispose;
400400
} else {
401401
disposeRef.current = null;
@@ -405,7 +405,9 @@ function tastyElement<K extends StyleList, V extends VariantMap>(
405405
disposeRef.current?.();
406406
disposeRef.current = null;
407407
};
408-
}, [injectionResult]);
408+
}, [directResult.rules, cacheKey]);
409+
410+
const injectedClassName = allocatedClassName;
409411

410412
let modProps: Record<string, unknown> | undefined;
411413
if (mods) {

0 commit comments

Comments
 (0)