CSS-in-JS: Comparing compile-time and runtime approaches

hamed-fatehi

Hamed Fatehi

Posted on May 12, 2023

CSS-in-JS: Comparing compile-time and runtime approaches

Integrating CSS into JavaScript, also known as CSS-in-JS, has become an increasingly popular approach in web development in recent years. Styles are defined directly within JavaScript code. In the context of CSS-in-JS, run-time methods generate styles at application runtime, while compile-time methods generate styles during the build process.



// linaria (Compile Time CSS extraction):
<style type="text/css" data-vite-dev-id="/src/App_1k77ml9.css">
    .t1uehvp8{color:var(--t1uehvp8-0);}
</style>
<h1 class="t1uehvp8" style="--t1uehvp8-0:#0f0;">
    Hello World!
</h1>


Enter fullscreen mode Exit fullscreen mode

In the compile-time approach (here Linaria), the CSS code is generated and extracted during the build process. The resulting CSS class (here .t1uehvp8) is inserted into the HTML code in the <style> tag, while the component is given a class with the generated class name. A custom CSS variable (here --t1uehvp8-0) is also used here to enable dynamic values.



// styled-components (Runtime Styling)
<style data-styled="active" data-styled-version="5.3.10">
    .bpatyH{color:#f00;}
</style>
<h1 color="#f00" class="sc-bgqQcB bpatyH">
    Hello World !
</h1>


Enter fullscreen mode Exit fullscreen mode

In contrast, the runtime approach (styled components here) generates the CSS code at runtime of the application. The resulting CSS class (here .sc-bgqQcB bpatyH) is dynamically created and assigned to the component. The CSS code is not pre-inserted in the HTML code, it is added at runtime. (for this example I set SC_DISABLE_SPEEDY = true so that the styles are visible in the DOM. More about speedy mode )

Benefits of CSS-in-JS

Before we dive into comparing runtime and compile-time approaches, let me first explain the general benefits of CSS-in-JS.

  • Encapsulation (Scoped CSS): CSS-in-JS allows for the isolation of styles at the component level, reducing the risk of style conflicts and unwanted side effects.

  • Dynamic styling: Because styles are defined directly in JavaScript, they can be easily created dynamically and changed based on application data or user interaction.

  • Ease of maintenance: The tight coupling of components and their associated styles makes it easier to find and update styles, especially in larger projects.

  • Dead Code Elimination: With CSS-in-JS, only the CSS code that is actually used is included in the application, which leads to a reduction in unused code and better performance.

  • Component-based design (colocation): Integrating styles into JavaScript components encourages the development of modular and reusable user interfaces.

Advantages of runtime solution

Here I use Styled-Components as an example. But most of the similar libraries share these properties. I'm just listing the benefits. For more information see the Styled-Components documentation.

  • Automatically critical CSS
  • No class name bug
  • Easier deletion of CSS
  • Simple dynamic styling
  • Painless maintenance
  • Automatic vendor prefixing

But like (almost) everything else in life, good things come at a price.

Welcome to the Dark Side

I read this article a while ago: Why We're Breaking Up with CSS-in-JS . It was written by the developer of Emotion (Emotion is runtime like styled-components). In short, the author calls the following disadvantages:

  • CSS-in-JS increases run-time overhead: when components are rendered, the CSS-in-JS libraries have to convert their styles into plain CSS that can be injected into the document. This consumes additional CPU cycles and can affect the performance of your application.

  • CSS-in-JS increases the size of your bundle: Every user who visits your website must now download the JavaScript for the CSS-in-JS library. Emotion is 7.9 kB minzipped and Styled-Components is 12.7 kB. Both libraries aren't huge, but it adds up.

  • Frequently injecting CSS rules forces the browser to do a lot of extra work: the runtime CSS-in-JS libraries work by injecting new style rules when components are rendered, and at a fundamental level this is bad for performance

  • CSS-in-JS confuses the React DevTools: for each element using the CSS property renders emotion <EmotionCssPropInternal> and <Insertion> components. If you use the CSS property on many elements, Emotion's internal components can mess up the React DevTools.

In the end, the author suggests using Sass modules and utility classes instead, as the runtime performance cost of Emotion far outweighs the developer experience (DX) benefits.

There is also a nice article that compares CSS and CSS-in-JS in a real application. Let's look at some charts from this article:

Lighthouse performance audit:
Lighthouse performance audit

Performance profiling:
Performance profiling

I also created a dummy react app that renders 3000 colored boxes and 3000 colored short texts. In this app, you can change the theme with a click of a button. Below is the interactivity test of Linaria (a compile time library) and styled components. The test was recorded when you click the "Theme change" button. The result shows a dramatic difference in performance. But as you know, this was an exaggerated example to show the difference.

Styled-Component (Runtime-Time CSS-in-JS) Interaction duration : 3.312 ms
Styled-Component (Runtime-Time CSS-in-JS) Interaction duration : 3.312 ms

Linaria (Compile-Time CSS-in-JS) Interaction duration : 528 ms
Linaria (Compile-Time CSS-in-JS) Interaction duration : 528 ms

Compile-time CSS-in-JS

Compile-time CSS-in-JS tools like Linaria offer an intermediate solution between pure CSS-based solutions and runtime CSS-in-JS approaches. Linaria combines the best of both approaches by generating the CSS classes at compile time and extracting them into separate CSS files instead of generating them at runtime. This makes it possible to avoid the performance penalties of runtime CSS-in-JS solutions while retaining the benefits of component isolation and dynamic style matching.

Advantages are:

  • No Runtime Overhead: Compile-time CSS-in-JS solutions generate static CSS, eliminating the need for JavaScript to handle styles at runtime. This reduces CPU load and improves performance.

  • Smaller bundle size: Since the CSS code is extracted at compile time, no additional JavaScript library is required at runtime. This results in a smaller bundle size and faster loading times.

  • Cleaner React DevTools: Since the styles are extracted into static CSS and no additional components are created at runtime, the React DevTools view remains tidy and uncluttered.

  • Optimum browser performance: Static CSS extraction prevents the browser from constantly having to calculate new CSS rules, since the styles are already generated at compile time. This leads to better browser performance.

  • Simplified Server-Side Rendering (SSR): Since the CSS code is static, there are fewer problems using SSR. Static extraction avoids potential complications that might arise with dynamic CSS-in-JS solutions.

  • Improved browser caching: Since the extracted style files are separate, they can be cached more effectively by the browser. Even if the JavaScript changes, the static CSS files can still remain in the cache, resulting in faster load times and a better user experience.

What about SSR (Server Side Rendering)?

Almost all CSS-in-JS libraries support SSR , even the compile-time libraries. For example, Styled-Components uses ServerStyleSheet to generate critical css on the server, which can be made available to the client in a blocking request. This avoids the Flash of Unstyled Content (FOUC).

This screenshot from a NextJS application using styled components with ServerStyleSheet shows how style tags are extracted and made available to the client:

Styled components with ServerStyleSheet:
styled components with ServerStyleSheet

This feature could, to some extent, compensate for the lack of generated CSS at compile time. A major benefit of extracted CSS before runtime is that you can use tools like critical or penthouse to automatically extract the critical CSS (above the fold) and serve only that style via a blocking request, while the rest asynchronously in the background is loaded.

Conclusion

The following points are the most important findings:

  • CSS-in-JS compile-time methods seem to perform better, especially on low-powered devices.
  • The migration from a runtime to a compile-time solution can take place step by step and component by component. Tools like Linaria provide syntax similar to SC to smooth the way for migration.
  • One should only switch from the build-time to the compile-time solution when performance becomes a bottleneck. In many cases, end users may not notice the improvements.
  • Compile-time solutions require requests to get the style files. Therefore, it is important to think about a good caching mechanism. This is especially important on first-time visits, as there is no cache for style files in the browser.

Side note: This text focused on CSS-in-JS. Of course there are many other tools like utility first classes (e.g. Tailwind) that don't involve JS in styling. Other frameworks like Angular also take a similar approach, combining Sass and component-level styling. From experience I can say CSS-in-JS provides better DX. Writing complex logic in Scss could be much harder than doing it in JS. But again, in your case the styling might not need such level of dynamics.

Styling in general is evolving, and there are many ways of thinking. Let me know what tools you use in your projects. πŸ˜‡

Thank you for reading and, as always, I appreciate your feedback. πŸ₯³

Cover Photo: Dall-E (Oil pastel drawing of 2 aliens putting makeup on each other)

πŸ’– πŸ’ͺ πŸ™… 🚩
hamed-fatehi
Hamed Fatehi

Posted on May 12, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related