12 February, 20236 minute read

CSS-in-JS to Tailwind: 36% better web vitals

The first version of this website was a slapdash effort to put something up while looking for additional engineering opportunities. Initially it was just a landing page, and it served its purpose well. As time went on I’ve layered on a variety of features like blog posts to that shaky foundation.

While styled-components enabled me to reuse code from another project and get this site shipped quickly, it’s not the optimal choice for growing into the future. I am not a designer, and the design “system” I came up with for sophiabits.com lacked good vertical rhythm which hurt readability and made things look amateur.

I’ve also been concerned about the performance overhead of CSS-in-JS for something as simple as a portfolio site—while my site loads quickly, it’s less thanks to my engineering effort and more to how slow the web is in general.

I’ve been steadfastly ignoring the urge to rewrite for about half a year now, but when Next.js 13 launched in October last year I knew I wouldn’t be able to push it out for much longer. React server components are a game changer, and will allow me to produce far richer blog posts without needing to eat large bundle size increases.

The problem, of course, is that CSS-in-JS solutions don’t work well (if at all) inside server components. If I want to benefit from server components, or the newly released font optimization feature then I need to get off of styled-components and on to something else.

To that end, I spent a good chunk of last Saturday rewriting everything with Tailwind CSS. I could have done it with any CSS-based technology (SASS modules have worked well for me in the past), but functional CSS has a lot going for it and the official typography plugin both saved me a lot of time while also looking far nicer than anything I could have hoped to make on my own.

Lighthouse results

The rewrite achieves the two things I wanted: the website looks a lot nicer now that everything is built according to the excellent design system provided by Tailwind, and I’m unblocked on upgrading from Next.js 12 to Next.js 13. But how did it impact on performance?

To measure performance metrics, I ran next build and next start to start up a production version of my website locally. "Request size" represents the amount of data transferred over the wire according to Chrome's network tab following a hard refresh of the homepage, and the remaining stats were collected by running a mobile Lighthouse test.

Here are my results before and after the rewrite:

TechnologyRequest sizeFCPTTISITBTLCPCLS
styled-components136 kB1.1s2.7s1.1s80ms1.5s0.003
Tailwind116 kB1.3s2.2s1.3s10ms1.6s0.011

The stat that stands out to me is bundle size: a 20 kB reduction is huge win! Time to Interactive has also reduced dramatically.

The other metrics are decidedly less impressive. +0.2s First Contentful Paint, and +0.1s Largest Contentful Paint isn’t what I was aiming for, and Cumulative Layout Shift has almost quadrupled.

What gives?

Network requests are slow

All of the CSS-in-JS solutions have an API available on the server for "collecting" the emitted styles so that you can then inline them directly into the page. Combined with your HTML being SSR'd, this makes your first paint really fast because the browser has all of the styling and markup available as soon as the page loads. You can read about styled-components' SSR implementation here on their documentation site.

Compare this with typical CSS-based approaches where you'll typically build a bundled .css file, and then point to it via a <link> tag in your markup. Even though in this case you wind up with a smaller bundle and less Javascript, the browser isn't guaranteed to have the styling information available as soon as the page loads because it needs to make an extra network request to fetch the CSS file.

If that CSS file is in the <head> of the page, the browser will treat it as a render-blocking resource and not draw anything to the page until it has loaded1. On slow networks like the one simulated in Lighthouse tests, this means your First Contentful Paint metric will suffer compared to CSS-in-JS.

This is a well-known advantage of CSS-in-JS tools, and a co-creator of styled-components has written about this before:

CSS-in-JS automatically extracts the critical CSS for the requested page and inlines it into a <style> tag. That means the first-paint with CSS-in-JS will always be faster as it saves both an extra network request for the .css file as well as sending less CSS code to the client.

Max Stoiber

Fortunately, however, there's a Next.js experiment called optimizeCss which gives us the benefits of critical CSS inlining without needing to pay the cost of a CSS-in-JS library.

Tailwind + inlined CSS

Opting in to the experiment is simple. All we need to do is enable the optimizeCss file by making the following change to next.config.js, and install the critters library.

Click to copy
module.exports = {  // ...  experiments: { optimizeCss: true },};

Once we've done that, we can remeasure our web vitals.

TechnologyRequest sizeFCPTTISITBTLCPCLS
styled-components136 kB1.1s2.7s1.1s80ms1.5s0.003
Tailwind116 kB1.3s2.2s1.3s10ms1.6s0.011
Tailwind (inlined)123 kB0.7s2.2s0.7s40ms1.0s0.013

Overall, the inlined Tailwind version is kind of like a weird hybrid. Our bundle size reduction isn't quite as impressive (-13 kB instead of -20 kB2), but our FCP is extremely low--it's a 36% reduction compared to styled-components. Speed Index and Largest Contentful Paint have seen similarly massive improvements.

I'm not entirely sure why Cumulative Layout Shift is so high here, but it's something I'm motivated in getting to the bottom of. Overall I'm not too worried about it--the page loads quickly, so the layout shift isn't too noticeable on typical hardware--but it's less than ideal.

It's also worth mentioning that there's still room for improvement here, although without platform support from Vercel or moving to self-hosted infrastructure it won't be possible to implement myself. While the critical CSS for each page has been inlined into the built pages, there's still a <link> element pointing to the compiled Tailwind CSS file. Ideally we'd only include the inlined CSS when someone comes to sophiabits.com for the very first time, and then omit the inlined CSS from any page navigated to thereafter under the assumption that the CSS file has been downloaded and cached by the user. That way, subsequent page loads wouldn't be encumbered by the ~7 kB of CSS inlined into each page.

Overall, though, I'm quite happy with the overall shift in performance metrics.

Should you switch?

It depends. I really, really like canned component libraries for getting business applications out of the door, and my favorite canned component library is MUI. Their styled system gives you a lot of levers for customizing the look and feel of its components, and that ergonomic API is made possible in large part by CSS-in-JS.

Likewise for highly dynamic sites where you need to be able to change theme easily, or even nest theme definitions on one page (we did this back at Kwotimation for the WYSIWYG website builder, for instance). It's hard to get that degree of flexibility with pure CSS, although CSS variables do help a lot.

But plain CSS definitely has its place, and the performance gains are compelling. Whilst Tailwind doesn't come packaged with a component library, it's fast to build with and there are a lot of extremely high quality headless components out there like downshift. For completeness' sake, there are also third-party component libraries built on top of Tailwind such as Flowbite, but because I've never used one I can't comment on their quality.

My recommendation would be something like this: if you're sensitive to performance (think marketing sites or ecommerce) or have unique UI requirements which aren't satisfied by off-the-shelf components, then go with Tailwind. If you're looking to build something fairly generic like an admin dashboard or need to do exotic things with theme management and aren't worried too much about performance, then feel free to reach for CSS-in-JS solutions.

  1. And if it isn't in the <head>, then you'll just suffer from a flash of unstyled content instead--so there's no easy victory here.
  2. According to The Shift Project's Lean ICT report, transferring 1 byte of data consumes approximately 1.52 x 10^-10 kWh. If you live in Germany where producing 1 kWh of energy emits 425g of CO2, then that means over 1,180,000 page loads a 13 kB page size saving results in a smidge over a kilogram of saved CO2. It's something, right?

Don't want to miss out on new posts?

Join 100+ fellow engineers who subscribe for software insights, technical deep-dives, and valuable advice.

Get in touch 👋

If you're working on an innovative web or AI software product, then I'd love to hear about it. If we both see value in working together, we can move forward. And if not—we both had a nice chat and have a new connection.
Send me an email at hello@sophiabits.com