Skip to main content

Real-world CSS vs. CSS-in-JS performance comparison

CSS-in-JS has taken a solid place in front-end tooling, and it seems this trend will continue in the near future. Especially in the React world. For example, out of 11492 people who participate in State of CSS survey in 2020 only 14.3% didn’t hear of Styled Components (a dominant CSS-in-JS library). And more than 40% of participants have used the library.

I wanted to see an in-depth performance comparison of CSS-in-JS libraries like Styled Components and a good old CSS for a long time. Sadly I was unable to found a comparison on a real-world project and not some simple test scenario. So I decided to do it myself. I have migrated the real-world app from Styled Components to Linaria, which will extract CSS on build time. No runtime generation of the styles on the user’s machine.

A short notice, before we begin. I’m not a hater of CSS-in-JS. I admit they have great DX, and the composition model inherited from React is great. It can provide developers with some nice advantages like Josh W. Comeau highlights in his article The styled-components Happy Path. I also use Styled Components on several of my projects or projects I have worked on. But I wondered, what is the price for this great DX from the user’s point of view.

Let’s see what I have found.

TLDR: #

Don’t use runtime CSS-in-JS if you care about the load performance of your site. Simply less JS = Faster Site. There isn’t much we can do about it. But if you want to see some numbers, continue reading.

What I measured and how #

The app I have used for the test is a pretty standard React app. Bootstrapped using Create React App project, with Redux and styled using Styled components (v5). It is a fairly large app with many screens, customizable dashboards, customer theming, and more. Since it was built with CRA, it doesn’t have server-side rendering, so everything is rendered on the client (since it’s a B2B app, this wasn’t a requirement).

I took this app and replaced the Styled Components with Linaria, which seems to have a similar API. I thought the conversion would be easy. It turned out it wasn’t that easy. It took me over two months to migrate it, and even then, I have migrated only a few pages and not the entire app. I guess that’s why there is no comparison like this 😅. Replacing the styling library was the only change. Everything else remained intact.

I have used Chrome dev tools to run several tests on the two most used pages. I have always run the tests three times, and the presented numbers are an average of those 3 runs. For all the tests, I have set CPU throttling to 4x and network throttling to Slow 3G. I used a separate Chrome profile for performance testing without any extensions.

Run test:

  1. Network (size of the JS and CSS assets, coverage, number of requests)
  2. Lighthouse audits (performance audit with mobile preset).
  3. Performace profiling (tests for page load, one for drag and drop interaction)

Network comparison #

We will start with a network. One of the advantages of CSS-in-JS is that there are no unused styles, right? Well, not exactly. While you have active only the styles used on the page, you may still download unnecessary styles. But instead of having them in a separate CSS file, you have them in your JS bundle.

Here is a data comparison of the same home page build with Styled Components and Linaria. Size before the slash is gzipped size, uncompressed size is after it.

Home page network stats comparison
Styled Component Linaria
Total number of requests 11 13
Total size 361kB/1.8MB 356kB/1.8MB
CSS size 2.3kB/7.2kB 14.7kB/71.5kB
No. of CSS requests 1 3
JS size 322kB/1.8MB 305kB/1.7MB
No. of JS requests 6 6
Search page network stats comparison
Styled Component Linaria
Total number of requests 10 12
Total size 395kB/1.9MB 391kB/1.9MB
CSS size 2.3kB/7.2kB 16.0kB/70.0kB
No. of CSS requests 1 3
JS size 363kB/1.9MB 345kB/1.8MB
No. of JS requests 6 6

Even though our CSS payload increased quite a lot, we are still downloading fewer data in total in both test cases (yet the difference is almost neglectable in this case). But what is more important, the sum of CSS and JS for Linaria is still smaller than the size of the JS itself in Styled Component.

Coverage #

If we compare coverage, we get a lot of unused CSS for Linaria (around 55kB) compared with 6kB for Styled Component (this CSS is from npm package, not from the Styled Components itself). The size of the unused JS is 20kB smaller for Linaria compared to Styled Component. But the overall size of the unused assets is larger in Linaria. This is one of the trade-offs of external CSS.

Coverage comparison – Home page
Styled Component Linaria
Size of unused CSS 6.5kB 55.6kB
Size of unused JS 932kB 915kB
Total size 938.5k 970.6kB
Coverage comparison – Search page
Styled Component Linaria
Size of unused CSS 6.3kB 52.9kB
Size of unused JS 937kB 912kB
Total size 938.5k 970.6kB

Lighthouse performance audit #

If we are talking about performance, it would be a shame not to use Lighthouse. You can see the comparisons in the charts below (average from 3 LI runs.). Aside from Web Vitals, I have also include Main thread work (time to parse, compile and execute assets, the biggest part of this is JS, but it covers layout and styles calculation, painting, etc.) and JS Execution time. I have omitted Cumulative Layout Shift since it was close to zero, and there was almost no difference between Linaria and Styled Component.

Lighthouse performance audit comparison of home page. Linaria has better speed index and larges contentful paint by more that 800 milliseconds. And main thread work is is lower by 1.63 seconds. Lighthouse performance audit comparison of search page. Linaria has better speed index by 900 milliseconds and larges contentful paint by 1.2 seconds. Main thread work is is lower by 1.27 seconds.

As you can see, Linaria is better in most of the Web Vitals (lost once in CLS). And sometimes by a large margin. For example, LCP is faster by 870ms on the home page and by 1.2s on the Search page. Not only does the page render with normal CSS much faster, but it requires fewer resources as well. Blocking time and time necessary to execute all the JS are smaller by 300ms and roughly 1.3 seconds respectively.

Performace profiling #

Lighthouse can give you many insights on the performance. But to get into the details, the performance tab in the dev tools is the best bet. In this case, the performance tab confirms the Lighthouse results. You can see the details on the charts below.

Profiling comparison of the home page. Rendering and paint are almost identical. But Linaria spend almost 1 second less time on scripting. And have total blocking time smaller by more than 1.5 seconds. Profiling comparison of the search page. Rendering and paint are almost identical. But Linaria spend more than 1 second less time on scripting and have total blocking time smaller almost by than 1.5 seconds.

Screens build with Styled Component had more long-running tasks. Those tasks also took longer to complete, compared to the Linaria variant.

To give you another look at the data, here is the visual comparison of the performance charts for loading the home page with Styled Component (top) and Linaria (bottom).

Comparison of Chrome dev tools performance minimap chart of home page build with Styled Components and Linaria. Pages build with Linaria have a visually smaller amount of long-running task, loading finished earlier and had better FPS.

Comparing user interaction #

To compare user interaction as well, not only the page load. I have measured the performance of the drag and drop activity used to assign items into groups. The result summary is below. Even in this case, Linaria beat the runtime CSS-in-JS in several categories.

Drag and drop comparison
Styled Component Linaria Diff
Scripting 2955 2392 -563ms
Rendering 3002 2525 -477ms
Painting 329 313 -16ms
Total Blocking Time 1862.66 994.07 -868ms
Comparison of Chrome dev tools performance minimap chart of drag and drop interaction for pages build with Styled Components and Linaria. Linaria shows less long-running tasks and less JS to execute.

Conclusion #

That’s it. As you can see runtime CSS-in-JS can have a noticeable impact on your webpage. Mainly for low-end devices and regions with a slower internet connection or more expensive data. So maybe we should think better about what and how we use our tooling. Great developer experience shouldn’t come at the expense of the user experience.

I believe we (developers) should think more about the impact of the tools we choose for our projects. The next time I will start a new project, I will not use runtime CSS-in-JS anymore. I will either use good old CSS or use some build-time CSS-in-JS alternative to get my styles out of JS bundles.

I think build-time CSS-in-JS libs will be the next big thing in the CSS ecosystem as more and more libs are coming out (the last one being vanilla-extract from Seek). And big companies are heading this way as well, like Facebook with their styling lib).

Thanks for reading the article. If you have any suggestions or ideas how to make the article better or you simply like it, feel free to share and discuss it on Twitter.

Tomas Pustelnik

Front-end developer with focus on semantic HTML, CSS, performance and accessibility. Fan of great and clever design, tooling addict and neverending learner. Building Qjub in my free time and writing on this blog.