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.
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.
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.
- Network (size of the JS and CSS assets, coverage, number of requests)
- Lighthouse audits (performance audit with mobile preset).
- Performace profiling (tests for page load, one for drag and drop interaction)
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:
|Total number of requests||11||13|
|No. of CSS requests||1||3|
|No. of JS requests||6||6|
Search page network stats comparison:
|Total number of requests||10||12|
|No. of CSS requests||1||3|
|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.
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:
|Size of unused CSS||6.5kB||55.6kB|
|Size of unused JS||932kB||915kB|
Coverage comparison – Search page:
|Size of unused CSS||6.3kB||52.9kB|
|Size of unused JS||937kB||912kB|
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.
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.
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.
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).
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:
|Total Blocking Time||1862.66||994.07||-868ms|
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).