Static extraction in CSS-in-JS isn’t one-size-fits-all, and you don’t necessarily need traditional static extraction to build performant React apps. The best optimization is often the one that actually ships to production!
If you’ve worked with React, chances are that you’ve encountered the concept of CSS-in-JS. CSS-in-JS changed how we think about styling React components by enabling us to colocate styles with components. This allows us to leverage JavaScript’s full power for dynamic styling.
Some popular CSS-in-JS libraries used in industry are styled-components, Emotion, Astroturf and linaria.
See the previous post on The Ultimate Guide to Styling React Components for more details on the different approaches to styling React components.
Like any powerful tool, CSS-in-JS comes with trade-offs. Chief among them: performance overhead. In this article, we’ll explore how static extraction in CSS-in-JS can help mitigate these performance concerns, leading to faster and more efficient React applications.
When we write something like this with styled-components (for example):
const Button = styled.button`
background-color: #007bff;
color: white;
padding: 12px 24px;
border-radius: 4px;
&:hover {
background-color: #0056b3;
}
`;
What happens at runtime (i.e., when your code actually runs in the browser)? The styled-components library needs to parse the above template literal, generate a unique class name, inject the styles into the DOM, and attach that class to your component. Multiply that by every styled component in your app, and you can start to see where performance might become an issue.
Note: While we’ll use styled-components to illustrate the concept of static extraction throughout this article, it’s worth noting that styled-components doesn’t actually support traditional static CSS extraction. This is a deliberate design choice we’ll explore in detail later. However, understanding how static extraction would work with styled-components helps us grasp the broader concept and appreciate the different approaches taken by various CSS-in-JS libraries.
If we take a look at the above button example again, we’ll notice the styles are completely static (i.e., they don’t change based on runtime conditions). Yet, (generally speaking) they’re still computed at runtime.
Static extraction addresses this issue by analyzing CSS-in-JS code during build-time (i.e., when Vite, webpack or a similar bundler processes the code) to identify styles that don’t depend on props or state. Since these styles are considered static, they get “extracted” into regular CSS files, meaning our React app no longer needs to execute JavaScript for these styles at runtime!
In other words, instead of computing static styles in the browser, we precompute them and include them in regular CSS files. Our components still work exactly the same way, but now the browser doesn’t have to do all that work.
With this, we get the best of both worlds. We can keep writing CSS-in-JS the way we’re used to, but static styles get optimized away into regular CSS. Dynamic styles (i.e., the ones that actually need runtime computation) stay as CSS-in-JS.
Most CSS-in-JS libraries that support static extraction do it through Babel plugins or build-time transformations. Let’s look at how to set this up for styled-components. We’ll first install the babel-plugin-styled-components plugin:
npm install babel-plugin-styled-components
Then configure it in our Babel setup:
// babel.config.js
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
[
'babel-plugin-styled-components',
{
displayName: true, // Better debugging
pure: true // Enables static extraction
}
]
]
};
That pure: true
option tells the plugin to analyze our styled components and extract any styles that don’t depend on props or theme values. The extracted styles end up in CSS files that get loaded alongside our JavaScript bundles. This essentially achieves the same performance goal as traditional static CSS extraction, reducing runtime overhead and improving overall application efficiency.
You might be wondering: What happens when styles are partially dynamic? This is where static extraction gets clever. Consider this component:
const Card = styled.div`
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
${props => props.highlighted && `
border: 2px solid #007bff;
box-shadow: 0 4px 16px rgba(0, 123, 255, 0.2);
`}
`;
A smart static extraction setup will pull out the base styles (background
, border-radius
, padding
and default box-shadow
) into static CSS. The conditional styles that depend on the highlighted
prop remain as runtime CSS-in-JS. You get optimized performance for the common case while maintaining full dynamic capabilities.
The CSS-in-JS ecosystem has evolved different philosophies around static extraction. Let’s spend a little time in this section going into more detail on how these different libraries approach the concept of static extraction.
Though we used styled-components as the example to illustrate the concept of static extraction above, styled-components doesn’t officially support traditional static extraction. As one project member (co-creator) explained in a GitHub issue, they made this choice deliberately:
“We don’t support static CSS extraction… Static extraction doesn’t generate dynamic CSS, which means your page will either appear broken until the JS executes, or you’ll need to defer until the JS is loaded.”
Instead, styled-components focuses on:
The pure: true
option in babel-plugin-styled-components doesn’t actually extract CSS files; it marks components as side-effect free for better tree-shaking and dead code elimination.
Emotion initially supported static extraction but deprecated it in version 10. Their reasoning was pragmatic:
“As emotion has gotten more performant and features such as composition
have been added, static extraction has become less important… Libraries
such as linaria do static extraction well and the people working on them
are focused on this specific problem.”
When Emotion did support extraction, it worked like this:
// .babelrc
{
"plugins": [["emotion", { "extractStatic": true }]]
}
This would generate separate .emotion.css
files for styles without interpolations. However, it broke composition patterns and limited the library’s flexibility.
Linaria takes a completely different approach—it’s zero-runtime CSS-in-JS. Everything is extracted at build time:
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
const button = css`
background-color: #007bff;
color: white;
`;
const Button = styled.button`
padding: 12px 24px;
border-radius: 4px;
`;
Linaria compiles this to pure CSS files with no runtime overhead whatsoever. It even provides a collect
helper for critical CSS extraction:
import { collect } from '@linaria/server';
const { critical, other } = collect(html, css);
// critical: CSS needed for initial render
// other: CSS that can be loaded async
Astroturf sits somewhere between Linaria and traditional CSS-in-JS, offering multiple APIs for different use cases:
import { css, stylesheet } from 'astroturf';
// Single class extraction
const btnClass = css`
color: blue;
border: 1px solid blue;
`;
// Full stylesheet extraction
const styles = stylesheet`
.btn {
padding: 0.5rem 1rem;
}
.primary {
background-color: blue;
}
`;
For dynamic values, Astroturf cleverly transpiles interpolations to CSS custom properties:
function Button({ bgColor }) {
return (
<button
css={css`
background-color: ${bgColor};
`}
>
Click me
</button>
);
}
// Compiles to: background-color: var(--bgColor);
Static extraction in CSS-in-JS isn’t a one-size-fits-all solution. The ecosystem has evolved different approaches based on different priorities. styled-components chose developer experience and flexibility over extraction. Emotion tried extraction but found it limiting. Linaria and Astroturf went all in on build-time extraction.
The key insight? You don’t necessarily need traditional static extraction to build performant React apps. Modern CSS-in-JS libraries are pretty fast, and techniques like SSR, code splitting and critical CSS can often provide better real-world performance than naive static extraction.
Here’s how you can think about picking a styling option:
Go with styled-components + SSR when you value developer experience above all, you’re already doing server-side rendering, or your app has moderate performance requirements. The developer experience is unmatched, and the performance is good enough for most apps.
Choose Linaria or Astroturf when zero runtime overhead is critical, you’re building something performance-sensitive (think ecommerce or news sites), or you’re comfortable with build-time constraints. The trade-off is less flexibility for dynamic styling, but for many applications, that’s worthwhile.
Stick with CSS Modules or vanilla CSS when you want complete control over extraction, prefer separation of concerns or runtime CSS-in-JS feels like bringing a bazooka to a knife fight.
The future likely holds a middle ground: build tools smart enough to automatically extract what can be extracted while preserving the flexibility we’ve come to expect from CSS-in-JS.
Until then, choose the approach that best fits your project’s needs, and remember: The best optimization is often the one that actually ships to production!
Hassan is a senior frontend engineer and has helped build large production applications at-scale at organizations like Doordash, Instacart and Shopify. Hassan is also a published author and course instructor where he’s helped thousands of students learn in-depth frontend engineering skills like React, Vue, TypeScript, and GraphQL.