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.

Main-thread time rendering a 400-item product grid Bar chart comparing main-thread rendering time for the same grid styled with runtime CSS-in-JS, a zero-runtime library and CSS modules. Main-thread time rendering a 400-item product grid Runtime CSS-in-JS 186ms Zero-runtime (extracted) 112ms CSS modules 108ms Mid-range phone profile; same React components and markup.

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

javascript
// 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).

Styling approaches compared Comparison of runtime CSS-in-JS, zero-runtime CSS-in-JS and CSS modules on runtime cost, dynamic styling and authoring. Styling approaches compared Approach Runtime cost Dynamic values Co-location Runtime CSS-in-JS serialise + inject any prop in component Zero-runtime CSS-in-JS none via CSS variables in component CSS modules none via CSS variables sibling file Utility classes none class switching in markup

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.

Incremental migration away from runtime styles Order of steps for migrating from runtime CSS-in-JS to zero-runtime styling with minimal risk. Incremental migration away from runtime styles Profile find hot components CSS variables for dynamic values Hoist static styled components Migrate hot paths cards, rows, buttons Remove library when unused

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.

/html>