Back to TechSheets
The Great CSS-in-JS Exodus: Why GitHub Moved Back to Static CSS and What It Means for Your Architecture

The Great CSS-in-JS Exodus: Why GitHub Moved Back to Static CSS and What It Means for Your Architecture

Thanga MariappanSenior Architect
9 min read
Sep 28, 2026

The Great CSS-in-JS Exodus: Why GitHub Moved Back to Static CSS and What It Means for Your Architecture

As Senior Front-End Architects, we're constantly evaluating the tools and patterns that define our craft. Trends come and go, but fundamental performance and maintainability remain paramount. That's why the recent news from GitHub – their decision to fully migrate github.com away from CSS-in-JS and towards shipping "more CSS" – is more than just a headline. It's a seismic event that demands a deep, architectural examination.

Today, we'll dissect the 'why' behind this significant pivot, explore the often-hidden costs of CSS-in-JS, and outline the architectural implications of embracing a more traditional, build-time CSS strategy. This isn't just about GitHub; it's about learning from a large-scale re-architecture to inform your own front-end decisions.

The Rise and Reign of CSS-in-JS: A Brief Retrospective

To understand the exodus, we must first understand the allure. CSS-in-JS libraries like Styled Components and Emotion emerged to solve real, pressing problems in front-end development, particularly in the era of component-driven UIs:

The Promises It Made

  1. Colocation: Keep styles alongside components, making them easier to manage and reason about. No more endless style.css files.
  2. Scoped Styles: Automagically generate unique class names, preventing global style collisions. This was a godsend for large teams and complex applications.
  3. Dynamic Styling: Easily incorporate JavaScript props into CSS, enabling powerful, dynamic theming and conditional styling.
  4. Dead Code Elimination: The promise of only shipping the styles used by rendered components, potentially leading to smaller bundles.
  5. Simplified Build: No need for complex PostCSS setups or separate CSS build steps; it all lives within JavaScript.

These benefits were compelling, leading to widespread adoption across the industry. Developers loved the perceived simplicity and power of writing CSS directly within their JavaScript components. However, beneath the surface, a different story was unfolding.

The Unforeseen Costs and Performance Tax

GitHub's decision wasn't arbitrary. It was a calculated move driven by an accumulation of performance bottlenecks and developer experience challenges. The very features that made CSS-in-JS appealing also introduced significant overheads, especially at scale.

Runtime Overhead and Hydration Hell

Perhaps the most critical performance culprit is the runtime processing. Unlike static CSS files, CSS-in-JS requires JavaScript to parse styles, generate class names, and inject them into the DOM. This happens during runtime, directly impacting your user's browser.

On server-side rendered (SSR) applications, this issue is exacerbated. While CSS-in-JS libraries can extract critical CSS for the initial render, the client still needs to download the JavaScript, re-evaluate the styles, and rehydrate the component tree. This rehydration process, where the client-side JavaScript takes over from the server-rendered HTML, can be incredibly expensive. During this period, the UI might appear interactive but be unresponsive, leading to a poor Time To Interactive (TTI) and potentially a jarring user experience.

Consider this simplified lifecycle:

  1. Initial Server Render: HTML and critical CSS (extracted) are sent to the client.
  2. Client Download: Browser downloads JS bundles (including CSS-in-JS runtime and styles).
  3. Hydration: JS executes, re-creates virtual DOM, re-evaluates and injects styles, attaches event listeners.
  4. Interactive: Application becomes fully interactive.

The critical gap between step 2 and 3, where styles are re-evaluated, is where significant performance problems lurk, especially for large component trees or complex dynamic styles.

Bundle Size Bloat and Caching Inefficiencies

While CSS-in-JS promised dead code elimination, the reality often led to increased bundle sizes. You're not just shipping CSS; you're shipping the CSS and the runtime library needed to process it. Furthermore, if styles are coupled with JavaScript, your browser's caching strategy becomes less efficient. CSS typically changes far less frequently than JavaScript logic. By bundling them, any JS change might force a re-download of styles that haven't changed, undermining long-term caching benefits.

Developer Experience and Tooling Gaps

While convenient for local styling, debugging CSS-in-JS can be more complex. Generated class names are often unreadable (sc-bdfBwQ fXWqjR). Performance profiling becomes convoluted as style computation merges with JavaScript execution. Build times can also suffer due to the extensive transformations required.

In Summary: The Performance Tax

  • Increased JavaScript Parse/Execute Time: Delaying TTI.
  • Larger JavaScript Bundles: Slower download, higher memory usage.
  • Rehydration Costs: Duplicate work on the client, leading to unresponsive UIs.
  • Inefficient Caching: Styles coupled with JavaScript.
  • Potential for Flash of Unstyled Content (FOUC): If critical CSS extraction isn't perfect or fails.

For a platform like GitHub, serving millions of users globally, even milliseconds of performance degradation translate into significant impact on user experience, perceived speed, and potentially, conversion rates.

GitHub's Pivot: Embracing the "More CSS" Philosophy

Moving away from CSS-in-JS doesn't mean abandoning modern front-end principles. Instead, it represents a thoughtful embrace of well-established, performant CSS strategies, leveraging modern tooling to achieve the best of both worlds: component-driven development with the performance benefits of static CSS.

"Shipping more CSS" means compiling and delivering CSS as static .css files that the browser can download, parse, and render before JavaScript execution even begins. This is a return to fundamentals, optimized with contemporary techniques.

Architectural Pillars of the New Strategy

  1. Build-Time CSS Extraction: The core idea is to move style processing from runtime to build time. Tools like Webpack, Vite, or PostCSS are configured to extract all component styles into dedicated .css bundles.
  2. CSS Modules for Scoping: Instead of runtime-generated class names, CSS Modules provide local scope by transforming class names during the build process, like button_primary__abc12. This retains the component-level encapsulation without runtime overhead.
  3. Utility-First CSS (Likely): While not explicitly stated, many large organizations leverage utility-first frameworks (like Tailwind CSS or similar internal systems) for atomic, highly reusable styling. This often complements component-scoped CSS by handling common properties like spacing, typography, and flexbox layouts with minimal CSS footprint.
  4. Design Tokens Compiled to Custom Properties (CSS Variables): A modern design system approach often uses design tokens (e.g., color-brand-primary). These are then compiled into CSS Custom Properties (--color-brand-primary: #007bff;), allowing for dynamic theming and consistent styling without JavaScript intervention.
  5. Global Stylesheets for Baselines: Resets, global typography, and foundational layout styles still reside in traditional global stylesheets, carefully managed to avoid conflicts.

A Code Example: Before and After

Let's illustrate the shift with a simple button component:

Before: CSS-in-JS (Styled Components)

// components/Button.js
import styled from 'styled-components';

const StyledButton = styled.button`
  background-color: ${props => props.primary ? '#007bff' : '#f0f0f0'};
  color: ${props => props.primary ? 'white' : 'black'};
  padding: 10px 20px;
  border-radius: 5px;
  border: none;
  cursor: pointer;

  &:hover {
    opacity: 0.9;
  }
`;

export const Button = ({ primary, children }) => {
  return <StyledButton primary={primary}>{children}</StyledButton>;
};

After: Static CSS (CSS Modules + Utility Classes)

First, define your component-specific styles in a CSS Module:

/* components/Button.module.css */
.button {
  padding: var(--spacing-md) var(--spacing-lg);
  border-radius: var(--border-radius-sm);
  border: none;
  cursor: pointer;
  transition: opacity 0.2s ease-in-out;
}

.button:hover {
  opacity: 0.9;
}

.primary {
  background-color: var(--color-brand-primary);
  color: var(--color-text-inverted);
}

.secondary {
  background-color: var(--color-brand-secondary);
  color: var(--color-text-default);
}

Then, use it in your component:

// components/Button.js
import styles from './Button.module.css';

export const Button = ({ variant = 'secondary', children }) => {
  const buttonClass = ``styles.button`{styles[variant]}`;
  return <button className={buttonClass}>{children}</button>;
};

// Example usage with utility class for margin:
// <Button variant="primary" className="mt-4">Click Me</Button>
// (Assuming 'mt-4' is a utility class from a separate global stylesheet)

Notice the use of CSS variables (--spacing-md, --color-brand-primary) which are compiled from design tokens and defined in a global stylesheet or another CSS Module. This allows for dynamic theming without JavaScript at runtime.

The Migration Path and Trade-offs

Migrating a codebase the size of GitHub's is not trivial. It likely involves:

  • A Phased Approach: Identifying critical performance paths and components for early conversion.
  • Automated Tooling (where possible): While a full automated conversion is unlikely, static analysis tools can assist in identifying CSS-in-JS usage and potentially extract simple styles.
  • Design System Evolution: Ensuring a robust design token system that compiles to CSS variables, providing the necessary dynamism without JavaScript overhead.
  • Developer Education: Retraining developers on CSS best practices, modularity, and the new styling conventions.

Key Trade-offs

  • Loss of JS-Powered Logic in CSS: Complex, dynamic styling based on intricate JS logic might need to be re-evaluated and potentially re-architected with CSS Custom Properties, element attributes, or simpler class toggles.
  • Re-introducing CSS Build Step: This is managed by modern bundlers, but it's an explicit step that was somewhat abstracted away by CSS-in-JS.
  • Discipline: Enforcing naming conventions and modularity becomes more critical without the automatic scoping of CSS-in-JS.

Key Takeaways

  1. Runtime Costs Are Real: For large-scale applications, the runtime overhead of CSS-in-JS, particularly during SSR hydration, can significantly degrade performance and user experience.
  2. Static CSS Wins on Performance: Shipping pre-compiled .css files offers superior browser caching, faster initial render, and better Time To Interactive (TTI) by decoupling styling from JavaScript execution.
  3. Modern CSS is Powerful: With CSS Modules, Custom Properties (CSS Variables), and utility-first frameworks, developers can achieve component-scoped, dynamic, and maintainable styling without the need for CSS-in-JS runtimes.
  4. Architectural Simplicity is a Feature: Returning to a more traditional, build-time CSS approach simplifies debugging, tooling, and potentially lowers the barrier to entry for non-JavaScript developers working on styling.

What You Should Do Today

  • Evaluate Your Current Stack: If your application heavily relies on CSS-in-JS, especially with SSR, audit your Lighthouse scores. Pay close attention to TTI, Total Blocking Time (TBT), and Largest Contentful Paint (LCP).
  • Benchmark Key Pages: Set up performance monitoring for your critical user journeys. Understand the real impact of your current styling solution.
  • Explore Modern CSS: Familiarize your team with CSS Modules, CSS Custom Properties, and potentially utility-first CSS frameworks. These are not new, but their integration into modern build pipelines makes them incredibly powerful.
  • Consider a Phased Migration: For non-critical paths or new features, experiment with a static CSS approach. Observe the performance benefits and developer experience.
  • Champion Performance: Advocate for architectural decisions that prioritize core web vitals. GitHub's move is a powerful testament to the fact that even cutting-edge platforms must sometimes revisit foundational principles for optimal user experience.