The Performance Cost of Runtime CSS-in-JS
This guide is part of Critical CSS & Render-Blocking Resources, within Rendering & CSS Performance. Runtime CSS-in-JS libraries — styled-components, Emotion and similar — let developers write styles inside components, with props-based dynamic values. They work by generating CSS during rendering: each styled component serialises its style object or template string, hashes it into a class name, and injects a rule into a style sheet if it has not been seen before. On the server, the collected styles are inlined into the HTML.
That convenience has costs that show up mainly on the client: the library's JavaScript must download and run; serialisation and hashing run during every render of styled components; injecting new rules triggers style recalculation across the document; and during hydration, the same work runs again for the whole page. On slower devices, these costs are measurable in total blocking time and INP. Many teams have migrated to zero-runtime approaches that extract static CSS at build time, keeping component-scoped authoring without the runtime work.
Rapid Diagnosis
- Check bundle composition for styled-components, Emotion or similar packages and their size.
- Record a Performance profile of hydration and a typical interaction; look for library functions (serialise, hash, insertRule) in the flame chart.
- Look for frequent style recalculations right after component renders.
- Check for dynamic styles driven by props that change often (for example, on hover or scroll), which create new rules repeatedly.
Root Cause Analysis
1. Style generation at render time. Each render of a styled component computes its styles.
2. Rule injection. Inserting new CSS rules invalidates styles and triggers recalculation.
3. Hydration doubles the work. The client recomputes styles that the server already rendered.
4. Prop-driven styles. Values that change frequently generate many unique classes.
Step-by-Step Resolution
1. Measure the cost in your app
Profile hydration and key interactions with 4x CPU throttling. Sum self time in the library's functions and the style recalculations that follow injection. This gives a concrete budget for the migration decision.
2. Move frequently changing values to CSS variables
// Instead of generating a new class per progress value:
const Bar = styled.div`width: ${(p) => p.pct}%;`;
// Set a CSS variable inline and keep one static rule:
const Bar = styled.div`width: var(--pct);`;
<Bar style={{ '--pct': `${pct}%` }} />
// trade-off: inline custom properties are not visible in the stylesheet, which
// can make debugging slightly harder, but avoid rule churn completely.
3. Hoist static styles
Define styled components at module level (not inside render functions) and keep variants as a small set of static classes rather than arbitrary prop interpolation.
4. Migrate to zero-runtime styling
Libraries such as vanilla-extract, Linaria, Panda CSS and StyleX extract CSS at build time. CSS modules and plain CSS are other options. Migrate incrementally: start with components that render most often (list rows, cards, buttons).
Verification
Compare profiles before and after: scripting time during hydration and interactions should drop, and style recalculation after renders should shrink. Check JavaScript bundle size for the removed library. In the field, compare INP p75 and TBT on affected templates.
Worked Example: An E-commerce Product Listing
An e-commerce React app used a runtime CSS-in-JS library for all components. Profiling a category page with 48 product cards on a mid-range phone showed 140ms of hydration time inside style serialisation and injection, and filter interactions spent 60ms per change regenerating classes for cards with price-dependent styles. The team moved price-based styling to CSS variables and migrated card, button and grid components to a zero-runtime library. Hydration TBT fell by 180ms, INP p75 on category pages improved from 290ms to 210ms, and the JavaScript bundle shrank by 13KB compressed.
Server Components and Streaming
React Server Components and streaming SSR change the picture. Runtime CSS-in-JS libraries that rely on React context for style collection need special support for server components and streaming, and some do not support server components at all. Zero-runtime approaches emit plain CSS files and work naturally with streaming and server components. If you are planning a move to the App Router or a similar architecture, factor styling into the migration — it is often the hardest dependency to change later.
Estimating the Benefit Before Migrating
A migration is a significant investment, so estimate the benefit first. Pick the most frequently rendered component on a key page — a product card, a table row, a list item — and reimplement it with CSS modules or a zero-runtime library on a branch. Profile both versions on the same page with CPU throttling, rendering the full list and performing a typical interaction such as filtering. The difference in scripting and style recalculation time, multiplied by how often that component renders across the app, gives a realistic picture of the gain. If the difference is under a few milliseconds per interaction on a mid-range device, the migration is probably not your highest-value performance work; if it is tens of milliseconds, it likely is.
Common Mistakes
- Defining styled components inside render. A new component type on every render, with new styles each time.
- Interpolating frequently changing values. Endless rule generation.
- Assuming SSR removes the cost. Hydration recomputes styles on the client.
- Big-bang migration. Rewriting every component at once; migrate hot paths first.
Edge Cases
Theming. Runtime themes can be replaced with CSS custom properties set on a root element, which zero-runtime libraries support well.
Design systems shared across apps. Publish the design system with extracted CSS so consuming apps do not inherit the runtime.
Critical CSS. Runtime libraries inline styles for server-rendered components, which acts like critical CSS; after migration, ensure extracted CSS is inlined or loaded appropriately.
Animation libraries. Some animation libraries generate styles at runtime too; profile them separately.
FAQ
Is runtime CSS-in-JS always slow?
It adds overhead proportional to the number of styled components rendered and the amount of dynamic styling. Small apps may not notice; large, interactive apps on mid-range phones often do.
What is zero-runtime CSS-in-JS?
Libraries that let you author styles in JavaScript or TypeScript but extract them to static CSS at build time, so no styling code runs in the browser.
Does runtime CSS-in-JS affect LCP?
It can, through larger bundles and longer hydration, especially for client-rendered apps. Server-rendered pages with inlined styles usually see more effect on INP and TBT than on LCP.
Can I keep styled-components and still improve performance?
Yes: hoist components, use CSS variables for dynamic values, avoid prop interpolation in hot paths, and keep the library up to date.
How do I measure style injection cost?
In a Performance profile, look for the library's internal functions and the Recalculate Style events that follow DOM updates. Compare against a branch with a component migrated.
Which zero-runtime library should I choose?
Choose based on your framework and team preferences — type-safe authoring, atomic output, or familiar syntax. Performance differences among zero-runtime options are small compared with the difference from runtime libraries.
Does runtime CSS-in-JS work with the browser cache?
The generated styles are part of the HTML or created in the browser, so they are not cached as a separate file. Extracted CSS files can be cached long-term with hashed names, which helps repeat visits.
Related
- Removing unused CSS with PurgeCSS — reducing static CSS overhead.
- Reducing style recalculation cost — the rendering cost behind rule injection.
- React performance — wider React rendering costs.