Improving Next.js 14 Performance: Why I Switched from Emotion to Pigment-CSS

Jason Rundell
By: Jason Rundell
Published:

In late 2024, I decided I needed to refresh my personal website with the latest Next.js (migrated from version 13 to 14). With the new App Router architecture and Server Side Rendering, it turned into a significant rebuild with not much of my 2023 code being re-used.

Since my website has very little need for client-side rendering, I made it a goal to make as much code rendered server-side as possible. With Next.js 14, you can easily still get away with components rendering server-side, but there's a performance impact. Even with my simple little site, which boils down to just an about, blog, and projects page with fewer than 20 pages, I was hitting a 64/100 performance score on PageSpeed Insights. It was driving me nuts! I could easily reach 95+ by just not using any frameworks at all and going with pure HTML, CSS, and optimized images—but that doesn't really help with growth or marketing my capabilities. So, of course, I had to over-engineer it!

A significant refactor was done to change Emotion's CSS library (https://emotion.sh/) to Pigment-CSS (https://github.com/mui/pigment-css). The reason for the refactor was to move more of my components to be rendered server-side since only a few of them use React hooks like useState and useEffect. When I started this refactor using Emotion, I ran into render issues due to how CSS was being generated by Emotion on the client-side.

The refactor was not smooth. There are some quirks when working with Pigment-CSS and TypeScript. Some simple components migrated smoothly from Emotion to Pigment with the Emotion syntax:

const Component = styled.div`...`

to Pigment's syntax:

const Component = styled.div`...`

The same! Chef kiss. But some other components? Not so much. Let’s just say TypeScript errors and I became close companions, and I questioned my life choices at least once a day.

Dynamic styles is where you'll have to roll-up your sleeves when migrating from Emotion to Pigment. Here is an example (sorry, I don't have snippets nailed down quite yet):

Emotion TSX Eample: https://gist.github.com/jasonrundell/445b09b8afc1739c8c617b89f8eb0f3b

Pigment TSX Eample: https://gist.github.com/jasonrundell/83efa4f801f4bd2a0ab4a20510b7c55a

Why the Switch?

Let’s break it down. Emotion is a great library for many React projects, but it doesn’t play nice with server-side rendering (SSR) and React Server Components (RSC). Why? Because Emotion’s CSS is generated on the client. The server sends down a minimal HTML shell, and only once the client JavaScript kicks in does Emotion add the styles. The result: a jarring flash of unstyled content (FOUC), slower first paint times, and a hit to your performance score.

Pigment-CSS, on the other hand, generates CSS at build time. The CSS is embedded in the server-rendered HTML, meaning styles are ready to go when the browser starts rendering. No need for JavaScript to play catch-up. It’s faster, leaner, and just overall a better fit for the Next.js app router's server-side-first approach.

This aligns with the principles Josh W. Comeau outlines in his excellent breakdown of CSS in RSCs. Server-first styling improves core web vitals like First Contentful Paint (FCP) and Largest Contentful Paint (LCP), which PageSpeed Insights is particularly obsessed with.

What About Performance?

Let me tell you—moving from a 64/100 score to a 90+ wasn’t just a ‘nice to have’; it became a personal vendetta. Pigment-CSS was a game-changer here. Since styles are statically extracted, the browser doesn’t need to parse and execute JavaScript just to paint my site. This means that the server sends down fully styled HTML, and we get that sweet, sweet performance boost.

That’s not to say everything was smooth sailing. Pigment-CSS has some limitations compared to Emotion. For example, dynamic styles—styles that change based on component props—aren’t as seamless. With Emotion, you can write inline styles directly within your styled components. With Pigment, you often have to predefine styles or use more traditional CSS classes.

But this trade-off is worth it for SSR-heavy sites like mine. For client-heavy apps with a lot of interactivity, Emotion might still be the better choice. For a mostly static site like mine? Pigment was the right move.

Lessons Learned (AKA Mistakes Were Made)

  1. Don’t underestimate build-time styling

    CSS-in-JS tools are great until they aren't. For SSR-heavy projects, always consider the impact on performance.

  2. TypeScript will judge you

    TypeScript isn’t a fan of CSS-in-JS libraries in general. You’ll end up wrestling with types, especially if you rely heavily on dynamic props.

  3. PageSpeed isn’t everything, but it sure feels like it

    It's easy to obsess over those performance numbers. Remember, a perfectly optimized static site isn't a realistic goal if you're using modern frameworks. Balance is key.

In the end, I’m happy with how my site turned out. The performance boost was worth the refactor headaches, and I can sleep easier knowing that my server-rendered HTML is styled on first paint. I might even have fewer nightmares about PageSpeed Insights. Maybe.


Here's the GitHub Pull Request of my CSS migration: https://github.com/jasonrundell/jasonrundell2024/pull/8