Skip to content

Commit adfb7b3

Browse files
committed
fix(tasty): optimizations
1 parent d9b6643 commit adfb7b3

File tree

7 files changed

+197
-252
lines changed

7 files changed

+197
-252
lines changed

src/stories/StyleInjector.docs.mdx

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ A high-performance CSS-in-JS solution that powers the Tasty design system with e
1313
The Style Injector is the core engine behind Tasty's styling system, providing:
1414

1515
- **🔄 Hash-based deduplication** - Identical CSS gets the same className
16-
- **📊 Reference counting** - Automatic cleanup when components unmount
16+
- **📊 Reference counting** - Automatic cleanup when components unmount (refCount = 0)
1717
- **🎯 CSS nesting flattening** - Handles `&`, `.Class`, `SubElement` patterns
18-
- **🎬 Keyframes injection** - First-class `@keyframes` support with deduplication
19-
- **🧹 Safe bulk cleanup** - Unused styles are aged and cleaned up in partial batches
18+
- **🎬 Keyframes injection** - First-class `@keyframes` support with immediate disposal
19+
- **🧹 Smart cleanup** - CSS rules batched cleanup, keyframes disposed immediately
2020
- **🖥️ SSR support** - Deterministic class names and CSS extraction
2121
- **🌙 Multiple roots** - Works with Document and ShadowRoot
22-
- **🔒 DOM presence validation** - Prevents deletion of styles still active in DOM
22+
- **⚡ Non-stacking cleanups** - Prevents timeout accumulation for better performance
2323

2424
> **💡 Note:** This is internal infrastructure that powers Tasty components. Most developers will interact with the higher-level `tasty()` API instead.
2525
@@ -67,7 +67,7 @@ const result = inject([{
6767

6868
console.log(result.className); // 't-abc123'
6969

70-
// Cleanup when component unmounts
70+
// Cleanup when component unmounts (refCount decremented)
7171
result.dispose();
7272
```
7373

@@ -116,9 +116,9 @@ const animatedComponent = inject([{
116116
declarations: `animation: ${fadeIn} 300ms ease-in;`
117117
}]);
118118

119-
// Cleanup
120-
fadeIn.dispose();
121-
animatedComponent.dispose();
119+
// Cleanup (keyframes disposed immediately, CSS rules marked for batch cleanup)
120+
fadeIn.dispose(); // Immediate keyframes deletion from DOM
121+
animatedComponent.dispose(); // CSS rules marked unused (refCount = 0)
122122
```
123123

124124
### `configure(config): void`
@@ -129,11 +129,10 @@ Configures the global injector instance.
129129
configure({
130130
devMode: true, // Enable development features
131131
maxRulesPerSheet: 8000, // Cap rules per stylesheet
132-
unusedStylesThreshold: 500, // Trigger cleanup threshold
132+
unusedStylesThreshold: 500, // Trigger cleanup threshold (CSS rules only)
133133
bulkCleanupDelay: 5000, // Cleanup delay (ms)
134134
idleCleanup: true, // Use requestIdleCallback
135-
bulkCleanupBatchRatio: 0.5, // Clean up oldest 50%
136-
unusedStylesMinAgeMs: 10000, // Minimum age before cleanup
135+
bulkCleanupBatchRatio: 0.5, // Clean up oldest 50% per batch
137136
forceTextInjection: false, // Force textContent insertion
138137
nonce: 'csp-nonce', // CSP nonce for security
139138
});
@@ -193,23 +192,27 @@ const comp3 = inject([commonStyle]);
193192
// Style is kept alive while any component uses it
194193
comp1.dispose(); // refCount: 3 → 2
195194
comp2.dispose(); // refCount: 2 → 1
196-
comp3.dispose(); // refCount: 1 → 0, marked for cleanup
195+
comp3.dispose(); // refCount: 1 → 0, eligible for bulk cleanup
196+
197+
// Rule exists but refCount = 0 means unused
198+
// Next inject() with same styles will increment refCount and reuse immediately
197199
```
198200

199-
### Bulk Cleanup System
201+
### Smart Cleanup System
200202

201203
```typescript
202-
// Styles are not immediately deleted when dispose() is called
203-
// Instead, they're marked as unused and cleaned up in batches
204+
// CSS rules: Not immediately deleted, marked for bulk cleanup (refCount = 0)
205+
// Keyframes: Disposed immediately when refCount = 0 (safer for global scope)
204206

205207
configure({
206-
unusedStylesThreshold: 100, // Cleanup when 100+ unused styles
208+
unusedStylesThreshold: 100, // Cleanup when 100+ unused CSS rules
207209
bulkCleanupBatchRatio: 0.3, // Remove oldest 30% each time
208-
unusedStylesMinAgeMs: 5000, // Styles must be unused for 5s
209210
});
210211

211-
// This prevents performance issues from frequent DOM manipulation
212-
// and allows "recently used" styles to be quickly reactivated
212+
// Benefits:
213+
// - CSS rules: Batch cleanup prevents DOM manipulation overhead
214+
// - Keyframes: Immediate cleanup prevents global namespace pollution
215+
// - Unused styles can be instantly reactivated (just increment refCount)
213216
```
214217

215218
### Shadow DOM Support
@@ -292,10 +295,10 @@ configure({ devMode: true });
292295
const metrics = injector.instance.getMetrics();
293296

294297
console.log({
295-
cacheHits: metrics.hits, // Successful cache hits
298+
cacheHits: metrics.hits, // Successful cache hits
296299
cacheMisses: metrics.misses, // New styles injected
297-
unusedHits: metrics.unusedHits, // Reactivated unused styles
298-
bulkCleanups: metrics.bulkCleanups, // Cleanup operations
300+
unusedHits: metrics.unusedHits, // Current unused styles (calculated)
301+
bulkCleanups: metrics.bulkCleanups, // CSS cleanup operations (keyframes disposed immediately)
299302
stylesCleanedUp: metrics.stylesCleanedUp, // Total styles removed
300303
totalInsertions: metrics.totalInsertions, // Lifetime insertions
301304
});
@@ -364,13 +367,13 @@ configure({
364367
// The injector automatically manages memory through:
365368

366369
// 1. Hash-based deduplication - same CSS = same className
367-
// 2. Reference counting - styles stay alive while in use
368-
// 3. Aged cleanup - unused styles must remain unused for minimum time
369-
// 4. Partial cleanup - only oldest unused styles are removed per batch
370-
// 5. DOM validation - prevents cleanup of styles still in DOM
370+
// 2. Reference counting - styles stay alive while in use (refCount > 0)
371+
// 3. Immediate keyframes cleanup - disposed instantly when refCount = 0
372+
// 4. Batch CSS cleanup - unused CSS rules (refCount = 0) cleaned in batches
373+
// 5. Non-stacking cleanups - prevents timeout accumulation
371374

372375
// Manual cleanup is rarely needed but available:
373-
cleanup(); // Force immediate cleanup of eligible unused styles
376+
cleanup(); // Force immediate cleanup of all unused CSS rules (refCount = 0)
374377
destroy(); // Nuclear option: remove all stylesheets and reset
375378
```
376379

src/tasty/debug.ts

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type CSSTarget =
1010
| 'all' // tasty CSS + tasty global CSS (createGlobalStyle)
1111
| 'global' // only tasty global CSS
1212
| 'active' // tasty CSS for classes currently in DOM
13-
| 'cached' // tasty CSS present in sheets but not in DOM
13+
| 'unused' // tasty CSS with refCount = 0 (still in cache but not actively used)
1414
| 'page' // ALL CSS on the page across stylesheets (not only tasty)
1515
| string // 't123' tasty class or a CSS selector
1616
| string[] // array of tasty classes like ['t1', 't2']
@@ -33,7 +33,6 @@ interface InspectResult {
3333
interface CacheMetrics {
3434
hits: number;
3535
misses: number;
36-
unusedHits: number;
3736
bulkCleanups: number;
3837
totalInsertions: number;
3938
totalUnused: number;
@@ -45,12 +44,15 @@ interface CacheMetrics {
4544
rulesDeleted: number;
4645
}>;
4746
startTime: number;
47+
48+
// Calculated metrics
49+
unusedHits?: number; // calculated as current unused count
4850
}
4951

5052
interface CacheStatus {
5153
classes: {
52-
active: string[]; // from DOM scan
53-
cached: string[]; // present in sheets but not currently in DOM
54+
active: string[]; // classes with refCount > 0 and present in DOM
55+
unused: string[]; // classes with refCount = 0 but still in cache
5456
all: string[]; // union of both
5557
};
5658
metrics: CacheMetrics | null;
@@ -88,18 +90,18 @@ interface SummaryOptions {
8890
interface Summary {
8991
// Classes
9092
activeClasses: string[];
91-
cachedClasses: string[];
93+
unusedClasses: string[];
9294
totalStyledClasses: string[];
9395

9496
// Tasty CSS sizes
9597
activeCSSSize: number;
96-
cachedCSSSize: number;
97-
totalCSSSize: number; // tasty-only: active + cached + tasty global
98+
unusedCSSSize: number;
99+
totalCSSSize: number; // tasty-only: active + unused + tasty global
98100

99101
// Tasty CSS payloads
100102
activeCSS: string;
101-
cachedCSS: string;
102-
allCSS: string; // tasty-only CSS (active + cached + tasty global)
103+
unusedCSS: string;
104+
allCSS: string; // tasty-only CSS (active + unused + tasty global)
103105

104106
// Tasty global (createGlobalStyle)
105107
globalCSS: string;
@@ -449,13 +451,21 @@ export const tastyDebug = {
449451
} else if (target === 'active') {
450452
const activeClasses = findAllTastyClasses(root);
451453
css = injector.instance.getCssTextForClasses(activeClasses, { root });
452-
} else if (target === 'cached') {
453-
const activeClasses = findAllTastyClasses(root);
454-
const allClasses = findAllStyledClasses(root);
455-
const cachedClasses = allClasses.filter(
456-
(cls) => !activeClasses.includes(cls),
457-
);
458-
css = injector.instance.getCssTextForClasses(cachedClasses, { root });
454+
} else if (target === 'unused') {
455+
// Get unused classes (refCount = 0) from the registry
456+
const registry = (injector.instance as any)[
457+
'sheetManager'
458+
]?.getRegistry(root);
459+
const unusedClasses: string[] = registry
460+
? Array.from(
461+
registry.refCounts.entries() as IterableIterator<
462+
[string, number]
463+
>,
464+
)
465+
.filter(([, refCount]: [string, number]) => refCount === 0)
466+
.map(([className]: [string, number]) => className)
467+
: [];
468+
css = injector.instance.getCssTextForClasses(unusedClasses, { root });
459469
} else if (target === 'page') {
460470
css = getPageCSS({ root, includeCrossOrigin: true });
461471
} else if (/^t\d+$/.test(target)) {
@@ -536,15 +546,23 @@ export const tastyDebug = {
536546
const { root = document } = opts || {};
537547
const activeClasses = findAllTastyClasses(root);
538548
const allClasses = findAllStyledClasses(root);
539-
const cachedClasses = allClasses.filter(
540-
(cls) => !activeClasses.includes(cls),
549+
// Get unused classes (refCount = 0) from the registry
550+
const registry = (injector.instance as any)['sheetManager']?.getRegistry(
551+
root,
541552
);
553+
const unusedClasses: string[] = registry
554+
? Array.from(
555+
registry.refCounts.entries() as IterableIterator<[string, number]>,
556+
)
557+
.filter(([, refCount]: [string, number]) => refCount === 0)
558+
.map(([className]: [string, number]) => className)
559+
: [];
542560

543561
return {
544562
classes: {
545563
active: activeClasses,
546-
cached: cachedClasses,
547-
all: allClasses,
564+
unused: unusedClasses,
565+
all: [...activeClasses, ...unusedClasses],
548566
},
549567
metrics: injector.instance.getMetrics({ root }),
550568
};
@@ -699,7 +717,7 @@ export const tastyDebug = {
699717
const metrics = this.metrics({ root });
700718

701719
const activeCSS = this.css('active', { root, prettify: false });
702-
const cachedCSS = this.css('cached', { root, prettify: false });
720+
const unusedCSS = this.css('unused', { root, prettify: false });
703721
const allCSS = this.css('all', { root, prettify: false });
704722

705723
// Build cleanup summary from metrics
@@ -754,13 +772,13 @@ export const tastyDebug = {
754772

755773
const summary: Summary = {
756774
activeClasses: cacheStatus.classes.active,
757-
cachedClasses: cacheStatus.classes.cached,
775+
unusedClasses: cacheStatus.classes.unused,
758776
totalStyledClasses: cacheStatus.classes.all,
759777
activeCSSSize: activeCSS.length,
760-
cachedCSSSize: cachedCSS.length,
778+
unusedCSSSize: unusedCSS.length,
761779
totalCSSSize: allCSS.length,
762780
activeCSS: prettifyCSS(activeCSS),
763-
cachedCSS: prettifyCSS(cachedCSS),
781+
unusedCSS: prettifyCSS(unusedCSS),
764782
allCSS: prettifyCSS(allCSS),
765783
globalCSS: globalBreakdown.css,
766784
globalCSSSize: globalBreakdown.totalCSSSize,
@@ -781,14 +799,14 @@ export const tastyDebug = {
781799
` • Active classes (in DOM): ${summary.activeClasses.length}`,
782800
);
783801
console.log(
784-
` • Cached classes (performance cache): ${summary.cachedClasses.length}`,
802+
` • Unused classes (refCount = 0): ${summary.unusedClasses.length}`,
785803
);
786804
console.log(
787805
` • Total styled classes: ${summary.totalStyledClasses.length}`,
788806
);
789807
console.log(`💾 CSS Size:`);
790808
console.log(` • Active CSS: ${summary.activeCSSSize} characters`);
791-
console.log(` • Cached CSS: ${summary.cachedCSSSize} characters`);
809+
console.log(` • Unused CSS: ${summary.unusedCSSSize} characters`);
792810
console.log(
793811
` • Global CSS: ${summary.globalCSSSize} characters (${summary.globalRuleCount} rules)`,
794812
);
@@ -815,7 +833,7 @@ export const tastyDebug = {
815833
const hitRate =
816834
metrics.hits + metrics.misses > 0
817835
? (
818-
((metrics.hits + metrics.unusedHits) /
836+
((metrics.hits + (metrics.unusedHits || 0)) /
819837
(metrics.hits + metrics.misses)) *
820838
100
821839
).toFixed(1)
@@ -825,7 +843,7 @@ export const tastyDebug = {
825843

826844
console.log('🔍 Details:');
827845
console.log(' • Active classes:', summary.activeClasses);
828-
console.log(' • Cached classes:', summary.cachedClasses);
846+
console.log(' • Unused classes:', summary.unusedClasses);
829847
console.groupEnd();
830848
}
831849

@@ -984,7 +1002,7 @@ export const tastyDebug = {
9841002
console.log('📖 Common targets for css()/log():');
9851003
console.log(' • "all" - all tasty CSS + global CSS');
9861004
console.log(' • "active" - CSS for classes in DOM');
987-
console.log(' • "cached" - CSS for unused classes');
1005+
console.log(' • "unused" - CSS for classes with refCount = 0');
9881006
console.log(' • "global" - only global CSS (createGlobalStyle)');
9891007
console.log(' • "page" - ALL page CSS (including non-tasty)');
9901008
console.log(' • "t123" - specific tasty class');

src/tasty/injector/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,6 @@ export type {
239239
KeyframesResult,
240240
KeyframesSteps,
241241
KeyframesCacheEntry,
242-
UnusedRuleInfo,
243242
CacheMetrics,
244243
} from './types';
245244

0 commit comments

Comments
 (0)