by Oleg Isonen
The tradeoffs of CSS-in-JS
Recently I wrote a higher level overview of CSS-in-JS, mostly talking about the problems this approach is trying to solve. Library authors rarely invest time into describing the tradeoffs of their solution. Sometimes it’s because they are too biased, and sometimes they just don’t know how the users apply the tool. So this is an attempt to describe the tradeoffs I have seen so far. I think it is important to mention that I am the author of JSS, so I should be considered biased.
A big part of the problem is our inability to communicate accurately the use cases where CSS-in-JS shines and how to use it properly for a task. Many CSS-in-JS enthusiasts have been successful at promoting the tech, but not many critics talked about the tradeoffs in a constructive manner, without taking cheap swings at the tools. As a result, we left many tradeoffs hidden and didn’t make a strong effort to provide the explanation and workarounds.
CSS-in-JS is an attempt to make complex use cases easier to handle, so don’t push it where it is not needed!
Besides the potential runtime overhead, you need to consider 4 different bundling strategies, because some CSS-in-JS libraries support multiple strategies and it is up to the user to apply them. *
Strategy 1: Runtime generation only
The tradeoff of runtime generation is the inability to provide styled content at the early stage, as the document starts loading. This approach usually fits for applications without content that can be useful immediately. Usually, such applications require user interactions before they can really become useful to a user. Often such applications work with content that is so dynamic that it becomes outdated as soon as you load it, so you need to establish an update pipeline early on, for example, Twitter. In addition, when a user is logged-in, there is no need to provide HTML for SEO.
Perceived performance of such applications can be improved with placeholders and other tricks to let the application feel more instant than it actually is. Such applications are usually data heavy anyways, so they won’t be useful as quickly as an article.
Strategy 2: Runtime generation with Critical CSS
Without Critical CSS, a static content-heavy single page application with runtime CSS-in-JS will have to show placeholders instead of content. This is bad because it could have been useful to a user much earlier, improving the accessibility on low-end devices and for low-bandwidth connections.
The tradeoff of this strategy is the cost of Critical CSS extraction and the cost of runtime CSS generation.
Strategy 3: Build-time extraction only
This strategy is the default one on the web without CSS-in-JS. Some CSS-in-JS libraries allow you to extract static CSS at build time.* In this case, no runtime overhead is involved, CSS is rendered on the page using a link tag. The cost of the CSS generation is paid once ahead of time.
There are 2 major tradeoffs here:
- You can’t use some of the dynamic APIs CSS-in-JS offers at runtime, because you have no access to the state. Often you still can’t use CSS custom properties, because they are not supported in every browser and cannot be polyfilled at build time by nature. In this case, you will have to do workarounds for dynamic theming and state-based styling.*
- Without Critical CSS and with an empty cache, you will block the first paint, until your CSS bundle gets loaded. A link element in the head of the document blocks the rendering of HTML.
- Non-deterministic specificity with page based bundle splitting in single page applications.*
Strategy 4: Build-time extraction with Critical CSS
This strategy is also not unique to CSS-in-JS. Full static extraction with critical CSS delivers the best performance when working with a more static application. This approach still has the aforementioned tradeoffs of a static CSS, except that the blocking link tag can be moved to the bottom of the document.
There are 4 main CSS rendering strategies. Only 2 of them are specific to CSS-in-JS and none of them apply to all libraries.
Developers need to take responsibility for accessibility. There is still a strong misguided idea that an unstable internet connection is a problem of economically weak countries. We tend to forget that we have connectivity issues every single day when we enter an underground rail system or a large building. A stable cable-free mobile connection is a myth. It's not even easy to have a stable WiFi connection, for example, a 2.4 GHz WI-FI network can get interference from a microwave oven!
The cost of Critical CSS with Server-side Rendering
To get Critical CSS extraction for CSS-in-JS, we need SSR. SSR is a process of generating the final HTML for a given state of an application on the server. In fact, it can be quite a complex and expensive process. It requires a certain amount of CPU cycles on the server for each HTTP request.
CSS-in-JS usually leverages the fact that it is hooked into the HTML rendering pipeline.* It knows what HTML was rendered and what CSS it needs so that it is able to produce the absolute minimal amount of it. Critical CSS adds additional overhead to HTML rendering on the server because that CSS also needs to be compiled into a final CSS string. In some scenarios, it is hard or even impossible to cache on the server though.
Rendering black box
In order to keep the source order specificity consistent, both above named libraries generate a new CSS rule if it contains a dynamic declaration and the component updates with new props. To demonstrate what I mean, I created this sandbox. In JSS we decided to take a different tradeoff, which allows us to update the dynamic properties without generating new CSS rules.*
Steep learning curve
Overall there is a learning curve, we can’t deny it. This learning curve is usually not much bigger, though, than learning Sass. In fact, I created this egghead course to demonstrate this.
Most CSS-in-JS libs are not interoperable. This means that styles written using one library can’t be rendered using a different library. Practically it means you can’t switch your entire application easily from one implementation to another. It also means that you can’t easily share your UI on NPM without bringing your CSS-in-JS library of choice into the consumer's bundle unless you have a build-time static extraction for your CSS.
We have started to work on the ISTF format that is supposed to fix this problem, but unfortunately we haven’t had time yet to get it to a production-ready state.*
I think sharing reusable framework agnostic UI components in the public domain is still a generally hard-to-solve problem.
It is possible to introduce security leaks with CSS-in-JS. Like with any client-side applications, you need to escape user input before rendering it, always.
This article will give you more insight and some defacing examples.
Unreadable class names
Some people still think it is important that we keep meaningful readable class names on the web. Currently, many CSS-in-JS libraries provide meaningful class names based on the declaration name or component name in development mode. Some of them even let you customize the class name generator function.
In production mode though, most of them generate shorter names for a smaller payload. This is a tradeoff the user of the library has to make and customize the library if needed.
Tradeoffs exist, and I probably didn’t even mention all of them. But most of them don’t universally apply to all CSS-in-JS. They depend on which library you use and how you use it.
* It will take a dedicated article to explain this sentence. Let me know on Twitter (@oleg008) about which one you would like to read more.