How Linear achieved ~30% faster page navigation by replacing runtime CSS-in-JS with compile-time StyleX — and what every React developer can learn from their approach.
"I've spent more tokens on this migration than I'd like to admit." That's Kenneth Skovhus, Linear engineer, opening his story about moving Linear's React applications from styled-components to StyleX. Over the past few months, Linear has been systematically migrating their codebase — and the results are compelling: roughly 30% faster render times when navigating between pages.
This isn't just another library swap. It represents a fundamental shift in how modern React applications handle styling — from runtime CSS-in-JS (where styles are generated and injected as the browser renders) to compile-time CSS-in-JS (where styles are pre-compiled during the build step). For anyone building React applications at scale, Linear's journey offers concrete lessons about performance, architecture, and the direction of the React ecosystem.
Linear's migration was driven by two converging forces: performance and maintenance.
Runtime CSS-in-JS libraries like styled-components generate CSS rules and inject them into the DOM while the application renders. This means users pay for style generation and rule injection on every page navigation. In a large application like Linear — a project management tool with complex UI surfaces — this overhead accumulates across dozens of components per page.
The performance cost is particularly noticeable during page transitions. When a user navigates between views in Linear, new components mount, styled-components generates their CSS, injects it into the document, and only then can the browser paint the new content. With StyleX, all of this work happens at build time — the generated CSS is a static asset the browser loads alongside the JavaScript bundle.
The decisive factor was styled-components entering maintenance mode. After upgrading to React 18, Linear felt the impact directly: React introduced useInsertionEffect to help CSS-in-JS libraries inject styles more efficiently during concurrent rendering, but styled-components never adopted it.
Kenneth Skovhus traced the stalled PR to adopt useInsertionEffect and connected with its author, Cody Olsen at Sanity. They tested Sanity's optimized fork — which Sanity's write-up frames as a "last resort." The fork is a lifeboat, not a long-term plan.
The migration was guided by five non-negotiables:
Linear evaluated most React-compatible styling libraries. The closest alternative was vanilla-extract, which offers solid static extraction and type safety. However, its API felt fragmented, and requiring separate styling files didn't match how Linear prefers to work.
StyleX — Meta's compile-time CSS-in-JS solution — checked every box. It keeps styles local to components, provides a small API surface, deterministic style resolution through atomic CSS classes, type-safe interfaces, and strict guardrails that make it harder to restyle components from the outside. It's actively maintained by Meta, used across most of Meta's web surfaces, and adopted by companies like Figma and Cursor.
| Dimension | styled-components | StyleX |
|---|---|---|
| Style generation | Runtime — generates CSS during render | Compile-time — generates atomic CSS during build |
| Runtime overhead | Style injection, rule parsing on every mount | Zero runtime — static CSS classes |
| Encapsulation | Open — any component can be wrapped with styled() |
Strict — deliberate barriers to external styling |
| Style resolution | CSS specificity-based — conflicts possible | Deterministic — last defined style wins predictably |
| Bundle impact | ~12 KB runtime library included in bundle | Zero runtime — only compiled CSS output |
| React Server Components | Incompatible — requires DOM access | Compatible — static CSS class names only |
| Maintenance status | Maintenance mode — no new features | Actively maintained by Meta engineering team |
| Migration difficulty | N/A (source) | High — requires codemod for large codebases |
The difference between the two approaches is clearest in code. Here's how a typical Linear component changed:
import styled from 'styled-components';
import { Issue } from './types';
const Container = styled.div`
padding: 12px 16px;
border-radius: 8px;
border: 1px solid ${props => props.$active ? '#0f766e' : '#e7e5e4'};
background: ${props => props.$active ? '#f0fdfa' : '#ffffff'};
cursor: pointer;
transition: all 0.2s ease;
&:hover {
border-color: #0f766e;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
`;
const Title = styled.h3`
font-size: 0.95rem;
font-weight: 600;
margin: 0 0 4px 0;
color: #1c1917;
`;
const Meta = styled.span`
font-size: 0.8rem;
color: #78716c;
`;
function IssueCard({ issue, active }: Props) {
return (
<Container $active={active}>
<Title>{issue.title}</Title>
<Meta>#{issue.number} · {issue.status}</Meta>
</Container>
);
}
import stylex from '@stylexjs/stylex';
const styles = stylex.create({
container: {
padding: '12px 16px',
borderRadius: 8,
border: '1px solid #e7e5e4',
cursor: 'pointer',
transition: 'all 0.2s ease',
},
containerActive: {
borderColor: '#0f766e',
backgroundColor: '#f0fdfa',
},
title: {
fontSize: 14,
fontWeight: 600,
marginBottom: 4,
color: '#1c1917',
},
meta: {
fontSize: 13,
color: '#78716c',
},
});
const hoverStyles = stylex.create({
container: {
borderColor: '#0f766e',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
},
});
function IssueCard({ issue, active }: Props) {
return (
<div
{...stylex.props(
styles.container,
active && styles.containerActive,
)}
onMouseEnter={(e) => {
stylex.merge(hoverStyles.container);
}}
>
<h3 {...stylex.props(styles.title)}>
{issue.title}
</h3>
<span {...stylex.props(styles.meta)}>
#{issue.number} · {issue.status}
</span>
</div>
);
}
The StyleX version is more explicit about styling — no magic template literals, no dynamic CSS generation at runtime. Every style is declared as a plain object and merged deterministically using stylex.props(). The active state is handled through conditional style composition rather than interpolating a template string.
Linear didn't attempt a manual migration — the codebase is too large and too complex. Instead, Kenneth Skovhus built a deterministic codemod: styled-components-to-stylex-codemod.
The complexity of the task is staggering. styled-components uses tagged template literals — essentially a Turing-complete language embedded in CSS strings. Dynamic values, expressions, conditional styles, and props-based calculations create an enormous number of possible patterns. As Skovhus notes, "the number of ways people express the same intent is enormous, and it's too easy to produce output that looks right but subtly isn't."
What makes this even harder: Linear doesn't use a design system. The migration forced them to pay down years of accumulated styling debt — removing flexibility from shared components, tightening APIs, and making components harder to restyle from the outside.
The codemod is now at 500+ pull requests, roughly 100,000 lines of migration tooling, an online playground, cross-file selector handling, and extensive regression coverage. This is a testament to both the scale of modern web applications and the complexity of CSS-in-JS patterns in practice.
Linear's migration signals a broader shift in the React ecosystem. Compile-time CSS is becoming the default approach for performance-critical applications. Here's what to consider for your own projects:
If you're running styled-components on a substantial codebase and noticing render-time jank, StyleX (or vanilla-extract) could deliver measurable improvements. The 30% navigation speedup Linear achieved is hard to ignore. However, the migration cost is real — expect to invest in tooling and pay down styling debt.
Starting fresh? Skip runtime CSS-in-JS entirely. StyleX, vanilla-extract, or even CSS Modules with a modern preprocessor offer better performance and fewer long-term maintenance concerns. If you're starting a Next.js project, consider whether you even need a styling library — Tailwind CSS and CSS Modules are first-class citizens.
If your application has a few dozen components and page navigation is snappy, the migration cost likely outweighs the benefit. But this is the right time to plan — if you anticipate significant growth, choosing a compile-time solution now saves a major migration later.
The styled-components → StyleX migration is part of a larger industry trend. Runtime CSS-in-JS emerged around 2016 as a reaction against global CSS — it solved real problems (scoping, co-location, dynamic styles). But as applications grew, the runtime cost became visible. Compile-time solutions represent the next evolution: keep the developer experience of CSS-in-JS (co-located styles, component-scoped, dynamic) but eliminate the runtime tax.
For my own projects, I choose the styling approach based on the application's needs. For performance-critical React applications, compile-time CSS provides the best of both worlds. If you're evaluating frameworks or styling systems for a new project, I'd be happy to discuss your requirements and recommend the right approach.
styled-components-to-stylex-codemod, now at 500+ PRs and ~100,000 lines of migration tooling. The codemod includes an online playground, cross-file selector handling, and regression coverage. Because Linear doesn't use a design system, much of the migration cost went into tightening component APIs and eliminating styling flexibility.Choosing a styling system is just one part of frontend architecture. If you're evaluating options for your React application — or planning a migration away from runtime CSS-in-JS — I can help. I'm a full-stack web developer with experience across React, Next.js, and various CSS methodologies. Based in Minsk and working worldwide, I provide free initial consultations to understand your project and recommend the right approach.
Tell me about your React application — I'll recommend the optimal styling architecture and provide a preliminary estimate. Free of charge.