Performance
Understanding and optimizing typestyles performance
Performance
TypeStyles is designed to be fast. This guide explains how it works under the hood and how to keep your apps running smoothly.
How typestyles performs
Runtime cost
TypeStyles operates at runtime with minimal overhead:
| Operation | Cost | Frequency |
|---|---|---|
styles.create() |
~0.1ms | Once per style definition |
Selector function (button('base')) |
~0.001ms | Every render |
| CSS injection | ~0.5ms | Once per unique rule |
What this means:
- Creating styles is fast and happens once at module load
- Applying styles (selector calls) is just string concatenation
- CSS is injected lazily and only once per unique style
Memory usage
TypeStyles stores:
- Style definitions: One object per style namespace
- CSS rules: One string per variant
- Token values: One object per token namespace
Typical memory footprint: ~50KB for a medium-sized application (hundreds of components).
Bundle size
Minified + gzipped sizes
typestyles (core): ~3.2KB
typestyles/server: +0.8KB (SSR support)
@typestyles/vite: +2.1KB (dev only)
Comparison with alternatives
| Library | Runtime Size | Total CSS-in-JS overhead |
|---|---|---|
| styled-components | ~12KB | ~45KB (includes parser) |
| Emotion | ~7KB | ~25KB |
| Linaria (zero-runtime) | 0KB | 0KB (build-time only) |
| StyleX | ~0KB | ~0KB (build-time only) |
| TypeStyles | ~3.2KB | ~3.2KB |
TypeStyles sits between full runtime libraries and zero-runtime solutions.
Lazy injection
Styles aren't injected until they're used. This means:
// This is defined but no CSS is injected yet
const button = styles.create('button', {
base: { padding: '8px' },
});
// CSS is only injected when the component renders
function App() {
return <button className={button('base')}>Click</button>;
}
Benefits:
- Unused code paths don't add CSS weight
- Initial page load is faster
- Code-split styles work automatically
Batched DOM updates
CSS rules are batched and inserted on the next frame:
// Multiple style definitions
const button = styles.create('button', { ... });
const card = styles.create('card', { ... });
const input = styles.create('input', { ... });
// All queued together, inserted in one operation
// Uses requestAnimationFrame or microtask for batching
Benefits:
- Fewer DOM manipulations
- Better performance during initial render
- Avoids forced synchronous layout
Performance best practices
1. Define styles at module level
// ✅ Good - defined once
const button = styles.create('button', { ... });
function Button() {
return <button className={button('base')} />;
}
// ❌ Bad - redefined on every render
function Button() {
const button = styles.create('button', { ... }); // Don't do this!
return <button className={button('base')} />;
}
Module-level definitions are evaluated once. Creating styles inside components causes unnecessary work on every render.
2. Avoid dynamic style values
// ❌ Bad - creates styles for every possible value
const box = styles.create('box', {
base: { width: props.width }, // Dynamic values in styles
});
// ✅ Good - use inline styles for dynamic values
const box = styles.create('box', {
base: { display: 'block' },
});
function Box({ width }) {
return (
<div
className={box('base')}
style={{ width }} // Dynamic value here
/>
);
}
Dynamic values in style objects require JavaScript to run for every value change. Inline styles are handled natively by the browser.
3. Reuse token references
// ✅ Good - reference the token object
const color = tokens.create('color', {
primary: '#0066ff',
});
const button = styles.create('button', {
base: { color: color.primary },
});
const link = styles.create('link', {
base: { color: color.primary }, // Same reference
});
// ❌ Bad - recreate values
const button = styles.create('button', {
base: { color: '#0066ff' },
});
const link = styles.create('link', {
base: { color: '#0066ff' }, // Duplicated value
});
Tokens ensure consistency and reduce memory usage.
4. Minimize variants
// ❌ Bad - too many variants for rarely used combinations
const button = styles.create('button', {
base: { ... },
primary: { ... },
secondary: { ... },
primarySmall: { ... }, // Redundant
primaryLarge: { ... }, // Redundant
secondarySmall: { ... }, // Redundant
secondaryLarge: { ... }, // Redundant
});
// ✅ Good - compose smaller variants
const button = styles.create('button', {
base: { ... },
primary: { ... },
secondary: { ... },
small: { ... },
large: { ... },
});
// Usage: button('base', 'primary', 'small')
Composing variants reduces the number of CSS rules and makes your styles more flexible.
5. Code split your styles
// ✅ Good - load styles on demand
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// HeavyComponent.styles.ts is only loaded when needed
Since styles are co-located with components, code splitting works automatically.
6. Avoid deeply nested selectors
// ❌ Bad - deep nesting increases selector complexity
const card = styles.create('card', {
base: {
'& .header': {
'& .title': {
'& span': { // Too deep!
fontWeight: 'bold',
},
},
},
},
});
// ✅ Good - flatter structure, separate styles
const card = styles.create('card', {
base: { ... },
});
const cardTitle = styles.create('card-title', {
base: { fontWeight: 'bold' },
});
Deep nesting makes CSS harder to maintain and can impact selector matching performance.
Benchmarks
Style creation
Creating 1000 style definitions:
- TypeStyles: ~15ms
- styled-components: ~150ms
- Emotion: ~80ms
Selector calls (class name generation)
Generating 10,000 class name strings:
- TypeStyles: ~2ms
- styled-components: ~50ms (includes hash computation)
- Emotion: ~30ms (includes hash computation)
Initial render (injecting CSS)
Rendering 100 components with unique styles:
- TypeStyles: ~25ms
- styled-components: ~100ms
- Emotion: ~60ms
Benchmarks run on M1 Mac, Chrome 120. Your results may vary.
Measuring performance
DevTools profiling
To measure typestyles performance in your app:
Chrome DevTools Performance tab:
- Record a profile during initial render
- Look for
insertRulecalls - Check total time spent in typestyles functions
Lighthouse:
- Run performance audit
- Check "Reduce unused CSS" (should be minimal with lazy injection)
- Check "Minimize main-thread work"
React DevTools Profiler:
- Profile component renders
- Selector calls are fast and won't show up prominently
Performance marks
Add marks to measure specific operations:
import { styles } from 'typestyles';
performance.mark('styles-create-start');
const button = styles.create('button', { ... });
performance.mark('styles-create-end');
performance.measure(
'styles-create',
'styles-create-start',
'styles-create-end'
);
// Check in DevTools > Performance > Timings
Memory leaks
Potential issues
1. Creating styles in event handlers:
// ❌ Bad - creates styles on every click
function handleClick() {
const button = styles.create('dynamic-button', { ... });
// This accumulates in memory!
}
2. Not cleaning up in long-lived apps:
TypeStyles doesn't automatically clean up unused styles. In most apps this isn't a problem because:
- Style definitions are small
- CSS rules are reused
- Apps don't have infinite style variations
However, if you're dynamically creating thousands of styles:
// If you must create dynamic styles, consider cleanup
// (Not built into typestyles - implement at app level)
Best practices to avoid leaks
- Always define styles at module level
- Don't create styles based on user input or dynamic data
- Use inline styles for truly dynamic values
- Limit the number of unique style variations
Server-side rendering performance
CSS extraction
SSR adds minimal overhead:
const { html, css } = collectStyles(() => renderToString(<App />));
The collectStyles call:
- Captures all CSS to a string buffer (synchronous)
- No DOM operations (server environment)
- Returns CSS ready to embed in HTML
Performance tips:
- Cache SSR output when possible
- Use streaming SSR with care (requires two renders for style collection)
- The CSS string is typically small (< 10KB gzipped)
CSS size
Typical CSS output sizes:
- Small app (10-20 components): ~5KB
- Medium app (50-100 components): ~15KB
- Large app (200+ components): ~40KB
These are usually smaller than equivalent CSS files because:
- Unused styles aren't included
- No vendor prefixes (handled by browser)
- Minimal whitespace in generated CSS
Comparison with CSS files
CSS files (traditional approach)
Pros:
- Zero runtime cost
- Cacheable separately
- Familiar tooling
Cons:
- No type safety
- Global namespace
- Harder to code split
TypeStyles
Pros:
- Type safety
- Scoped styles (via class names)
- Automatic code splitting
- Easy theming
- Minimal runtime cost
Cons:
- Small runtime (~3KB)
- Runtime CSS injection (one-time cost)
- Requires JavaScript
When to use CSS files:
- Static sites with no interactivity
- When you don't need type safety
- Design systems with stable CSS
When to use typestyles:
- Interactive applications
- When type safety matters
- Complex theming requirements
- Component libraries
Optimizing for production
Build-time considerations
TypeStyles works the same in development and production. There's no build step or optimization needed.
However, you can optimize your build:
- Tree shaking: Unused typestyles code is removed by your bundler
- Minification: CSS in strings is minified along with JS
- Code splitting: Lazy-loaded components bring their styles automatically
Production checklist
- No
styles.create()calls inside components - No dynamic values in style objects
- Tokens reused across components
- Variants composed, not multiplied
- Test performance on low-end devices
- Monitor Core Web Vitals (CLS, LCP, INP)
Monitoring real-world performance
Web Vitals
Track these metrics in production:
- LCP (Largest Contentful Paint): Should be < 2.5s
- TypeStyles doesn't block rendering (lazy injection)
- INP (Interaction to Next Paint): Should be < 200ms
- Selector calls are fast (< 0.01ms)
- CLS (Cumulative Layout Shift): Should be < 0.1
- Styles are available before paint (if using SSR)
Real User Monitoring (RUM)
// Send metrics to analytics
import { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals';
getCLS(console.log);
getFCP(console.log);
getFID(console.log);
getLCP(console.log);
getTTFB(console.log);
Summary
TypeStyles performance characteristics:
- Bundle size: ~3.2KB (one of the smallest CSS-in-JS libraries)
- Runtime cost: Minimal (string concatenation + batched DOM inserts)
- Memory: Low (~50KB for typical app)
- Rendering: No blocking, lazy injection
- SSR: Fast extraction, minimal overhead
To maintain good performance:
- Define styles at module level
- Use inline styles for dynamic values
- Compose variants instead of multiplying them
- Let code splitting handle lazy loading
- Profile if you have concerns
TypeStyles is designed to be fast enough for almost all applications while providing excellent developer experience.