Sam Magura, staff software engineer at Spot and active maintainer of the CSS-in-JS Emotion library, recently detailed why Spot abandoned the runtime CSS-in-JS library Emotion in favor of Sass modules: Runtime overhead, payload overhead, and server rendering issues contributed to a lesser user experience.
In his blog article, Magura first recalled the benefits of runtime CSS-in-JS.
Today’s web applications are often implemented as a set of collaborating components. When using runtime CSS-in-JS libraries, developers define the style of components together with the component markup and logic. It becomes harder to modify or delete the code for a component without correctly modifying or deleting the component style. This solves problems found in large applications littered with obsolete style rules that went undetected. Such applications are heavier to download and execute, negatively affecting user experience.
Scoping CSS rules strictly to the relevant component also makes it harder, if not impossible, to unwillingly impact the style of other components. In the absence of component scoping, the cascade and specificity rules of CSS may lead to style definitions that bleed to unrelated components.
Lastly, using a Turing-complete language such as JavaScript gives developers complete freedom in expressing the relationship between component style and component logic. This comes in handy when the component style is not static and should be dynamically updated in response to user actions or changes in the application environment.
Magura however concludes that, based on his study on Spot’s codebase, the benefits of CSS-in-JS outweigh the disadvantages:
So, that’s the reason we are breaking up with CSS-in-JS: the runtime performance cost is simply too high.
CSS-in-JS may negatively impact user experience with its runtime and payload overhead.
On the one hand, computing and updating styles dynamically at render time may lead to slower rendering. Magura compared the rendering time of a component of Spot’s codebase implemented with the runtime CSS-in-JS library Emotion to that from an implementation with Sass modules (compiled to plain CSS files at build time). The comparison revealed that, with Emotion, rendering time almost doubles (27.7 ms vs. 54 ms). Developers may refer to the blog article to review the experiment’s data, flame graph analysis, and more.
On the other hand, adding the CSS-in-JS library to the application code adds to the code bundle downloaded by the browser, possibly slowing down application startup. Emotion is around 8 KB (min. zipped), while styled-components, a popular alternative CSS-in-JS library, is 12 KB.
Interestingly, the dynamic insertion of CSS style rules performed by runtime CSS-in-JS libraries may not always play well with other parts of the ecosystem.
Regarding React 18, Sebastian Markage provided the following warning in a GitHub issue to developers using React’s concurrent rendering capabilities:
This is an upgrade guide for CSS libraries that generate new rules on the fly and insert them with
<style>
tags into the document. This would be most CSS-in-JS libraries designed specifically for React today - like styled-components, styled-jsx, react-native-web.NOTE: Make sure you read the section “When to Insert
<style>
on The Client”. If you currently inject style rules “during render”, it could make your library VERY slow in concurrent rendering.
Runtime CSS-in-JS may also affect server-side rendering optimizations. In an article on server streaming, Misko Hevery (creator of the Qwik framework), Taylor Hunt, and Ryan Carniato explained the following:
For example, CSS-in-JS (such as
emotion
) is a very popular approach. But if that approach means all components need to fully render before the<style>
tags can be output, then that breaks streaming, as the framework is forced to buffer the whole response.
Magura mentioned that a fair number of issues logged in Emotion’s GitHub project relate to server-side rendering (e.g., React 18’s streaming, rules insertion order). The reported issues may generate significant accidental complexity (i.e., the complexity linked to the solution rather than originating in the problem). They may also result in a negative developer experience.
While Magura reminds the reader that he restricted his experiment to the Emotion CSS-in-JS library and the Spot codebase, he anticipates that most of the reasoning may identically apply to other runtime CSS-in-JS libraries and other codebases.
Tomas Pustelnik provided a year ago another data point that goes in the same direction, though following a different methodology. In his blog article Real-world CSS vs. CSS-in-JS performance comparison, Pustelnik concludes:
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.
Popular build-time CSS-in-JS libraries include Linaria, Astroturf, and vanilla-extract. Last year Facebook introduced stylex, its own build-time CSS-in-JS library. Developers can also use CSS modules and the related ecosystem (PostCSS modules, Sass modules).
CSS-in-JS refers to a pattern where CSS rules are produced through JavaScript instead of defined in external CSS files. Two sub-patterns coexist. Runtime CSS-in-JS libraries, such as Emotion or Styled-components, dynamically modify styles at runtime, for instance by injecting style tags into the document. Zero-runtime CSS-in-JS is a pattern that promotes extracting all the CSS at build time.