FrontalCode All articles
Performance Optimization

Vanilla CSS Is Having Its Moment — And Your Bundle Size Will Thank You

FrontalCode
Vanilla CSS Is Having Its Moment — And Your Bundle Size Will Thank You

Photo: developer writing CSS code on laptop with stylesheets, via www.rollingstone.co.uk

A few years back, adopting a CSS-in-JS library felt like the obvious move for any serious React team. Styled-components, Emotion, Stitches — they promised scoped styles, dynamic theming, and colocation that made your component tree feel clean and self-contained. And for a while, the trade-offs seemed worth it.

Then the bills started arriving.

Runtime style injection adds overhead. Serialization costs add up. Server-rendered apps started showing hydration mismatches. And somewhere along the way, teams began quietly asking: are we making this harder than it needs to be?

That question has turned into a full-blown conversation across the frontend community, and the answer — for a growing number of teams — is yes.

What CSS-in-JS Actually Costs You

The core pitch of CSS-in-JS was always about developer experience: keep your styles next to your logic, get scoping for free, and use JavaScript to drive dynamic behavior. All legitimate goals. The problem is the implementation.

Runtime CSS-in-JS libraries generate and inject styles at render time. That means your browser is doing extra JavaScript work on every page load just to produce CSS that could have shipped as a static file. On fast connections and beefy machines, you might not notice. But on mid-range Android devices — which represent a huge chunk of real-world traffic — that overhead compounds.

Google's Core Web Vitals have made this painfully visible. Metrics like Interaction to Next Paint (INP) and Largest Contentful Paint (LCP) don't care how elegant your component architecture is. They measure what users actually feel, and runtime style injection has a habit of showing up in the wrong places on a performance profile.

Beyond raw performance, there's the framework coupling problem. CSS-in-JS libraries are almost universally tied to React's rendering model. The moment your team wants to experiment with a different framework, add an island architecture, or move logic to the edge, your styling layer becomes an anchor.

Modern CSS Doesn't Need JavaScript to Be Powerful

Here's the thing that often gets lost in the debate: CSS has changed. A lot.

Cascade layers (@layer) let you explicitly control specificity without fighting selector wars or relying on JavaScript to manage ordering. You can define a base layer, a component layer, and a utilities layer — and the browser respects that hierarchy without any runtime overhead.

Container queries (@container) solve the problem that pushed a lot of teams toward JS-driven responsive logic in the first place. Instead of writing JavaScript to measure a parent element and conditionally apply classes, you define breakpoints relative to a component's own container. Your card component doesn't need to know where it lives; it just responds to the space it has.

Add CSS custom properties (variables) with proper scoping, @scope for true style encapsulation, and the :has() selector for parent-aware styling, and you've got a toolkit that would have felt like science fiction five years ago.

What the Migration Actually Looks Like

If you're running styled-components or Emotion in a production app, a full rip-and-replace isn't realistic. Teams that are successfully moving away from CSS-in-JS tend to follow a phased approach.

Start at the boundaries. New components get written in CSS Modules or plain CSS from day one. Don't touch existing components until there's a clear reason — a performance issue, a refactor, a redesign. This keeps the migration from becoming a time sink.

Lean on CSS Modules for scoping. If the main thing you valued about CSS-in-JS was automatic scoping, CSS Modules give you that at build time with zero runtime cost. Most bundlers — Vite, webpack, Next.js — support them out of the box. The syntax change is minimal; the performance benefit is real.

Use design tokens as CSS variables. One of the strongest arguments for CSS-in-JS was theme access in JavaScript. You can replicate this by defining your design tokens as CSS custom properties on :root and consuming them anywhere — no JavaScript required, and they work across component boundaries naturally.

Audit before you migrate. Tools like Chrome DevTools' coverage panel and Lighthouse can show you where styling overhead is actually hurting. Not every CSS-in-JS usage is a problem; some of it is genuinely fine. Focus your migration energy where the data tells you to.

The DX Trade-off Is Real — But Manageable

It would be dishonest to say there's no developer experience cost to moving away from CSS-in-JS. Colocation is genuinely convenient. Having props drive styles directly in the same file feels natural in a component-first workflow.

But the tooling around vanilla CSS and CSS Modules has matured significantly. VS Code extensions give you autocomplete and linting for CSS custom properties. PostCSS plugins let you use future syntax today. And once your team gets comfortable with cascade layers and container queries, the need to reach for JavaScript to solve styling problems drops off fast.

There's also something to be said for the portability of plain CSS. A stylesheet doesn't care what framework renders it. That flexibility becomes increasingly valuable as the frontend landscape keeps fragmenting.

The Bottom Line

CSS-in-JS isn't going away entirely — there are valid use cases, and some teams have built architectures where the trade-offs make sense. But the default assumption that it's the sophisticated choice? That's fading.

Modern CSS is capable, performant, and no longer requires JavaScript as a middleman. If your team is carrying the weight of a runtime styling library and starting to feel it in your metrics, this is a good time to take a hard look at what you're actually getting for that cost.

Sometimes the smartest frontend decision is the simpler one.

All Articles

Related Articles

Stop Shipping Bloat: How a Performance Budget Transforms Your Frontend Workflow

Stop Shipping Bloat: How a Performance Budget Transforms Your Frontend Workflow

Picking Your Rendering Side: A Practical Guide to Server vs. Client Components

React, Vue, or Svelte? A No-Nonsense Framework Breakdown for Teams That Actually Ship

React, Vue, or Svelte? A No-Nonsense Framework Breakdown for Teams That Actually Ship