From Stylish to Sluggish: The Hidden Costs of CSS-in-JS

From Stylish to Sluggish: The Hidden Costs of CSS-in-JS
Photo by Nick Abrams / Unsplash

If you've used modern web frameworks like React, Angular, or Vue, you've probably encountered CSS-in-JS tools like Styled Components, Emotion CSS, or JSS.

These tools let you write CSS using JavaScript, which can be handy. However, CSS-in-JS has a big drawback: it's slower than regular CSS.

This isn't just an opinion – it's a fact.

Now, CSS-in-JS can be useful in some projects. But it's hard to make it as fast as plain CSS.

Let's examine why CSS-in-JS is slower, how some libraries try to fix this, and other related issues.

CSS parsing

CSS is just plain text, like many other programming languages.

You could write it in any text editor, even something basic like Notepad, and it would still work.

For instance:

p {
  color: blue;
}

This CSS is made up of regular text characters. The p is just the letter "p", whether it's in a .css file or a .txt file.

You can even use a .txt file as a CSS stylesheet in HTML:

<link rel="stylesheet" href="css.txt" />

This works because your browser doesn't care about the file extension. It reads the content of the file.

So why does this work?

The browser turns the CSS text into a format it can understand.

Converting CSS text into a form the browser can use isn't instantaneous. It takes some processing power and time, even if it's usually quick.

Think of it like this:

  • Your browser needs to read and understand your CSS.
  • This takes a bit of work from your computer's CPU.
  • The more CSS you have, the longer this process takes.

So, before your web page can use the styles, there's this brief moment where your computer is figuring out what all that CSS means.

For small stylesheets, you probably won't notice this delay. But for larger, more complex CSS files, this parsing time can add up and potentially slow down how quickly your page loads and displays correctly.

This process is called parsing, creating what's known as an Abstract Syntax Tree (AST).

Abstract Syntax Tree (AST)

An Abstract Syntax Tree (AST) is a way to show code structure as a tree. It helps us see how different parts of the code relate.

Let's use math as an example:

  • 9 - ((6 / 3) + 2)
  • (9 - (6 / 3)) + 2

These expressions use the same numbers, but the brackets change their meaning. An AST would show this difference clearly.

An AST has 2 main parts:

  1. Nodes: These hold the actual data (like numbers or operations).
  2. Edges: These show how nodes connect to each other.

In an AST, you'll see:

  • A root node at the top (the main operation)
  • Leaf nodes at the bottom (usually numbers or values that can't be broken down further)

The root node is the boss, and as you go down the tree, you see how smaller parts make up the whole expression.

This tree structure helps computers (and humans) understand the order of operations and how each part of the code fits together.

CSS parsing uses the same parsing mechanism.

Still, it's crucial where the CSS code is positioned. CSS parsing is an important step because it determines how your browser styles web elements.

CSS in <head> tag

Imagine you have a <style> tag in your HTML file:

<!doctype html>
<html>
  <head>
    <style>
      /* Some CSS rules */
    </style>
  </head>
  <body>
    <!-- Some web page content -->
  </body>
</html>

When the browser finds this stylesheet, it pauses loading the page content. It first parses all the CSS to prepare for styling. Only then does it display the page.

The whole HTML and CSS handling process looks like this:

This process prevents what we call a "Flash of Unstyled Content" (FOUC).

FOUC happens when a page briefly shows unstyled content before applying CSS styles.

CSS in <script> tag

Let's look at what happens when we put our CSS creation inside a <script> tag:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script>
      const styleTag = document.createElement('style');
      styleTag.innerText = `p { display: none; }`;
      document.head.append(styleTag);
    </script>
  </head>
  <body>
    <p class="hidden">Hello, I am hidden!</p>
  </body>
</html>

In this setup, we're using JavaScript in the <head> to make a <style> tag, also in the <head>. This means the browser has to:

  1. Parse the JavaScript
  2. Run the JavaScript to make the CSS
  3. Parse the CSS
  4. Finally, show everything on the page

Here is the process visually:

This isn't the best way, but it's not terrible.

However, it's not really how CSS-in-JS usually works in real life.

Most of the modern frameworks don't put their <script> tags in the <head>. Instead, they often do something like this:

<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="root"></div>
    <script>
      render(document.querySelector('#root'));
    </script>
  </body>
</html>

This way, the page waits until it finds the 'root' element before adding the framework's content.

So, let's see what changes when we move our <script> tag to the <body> instead of the <head>.

CSS in <body> tag

If the code is moved to the body tag, we add a new step to our process: the browser shows the DOM before it reads the JavaScript.

<!DOCTYPE html>
<html lang="en">
  <body>
    <p class="hidden">Hello, I am hidden!</p>
    <script>
      const styleTag = document.createElement('style');
      styleTag.innerText = `p { display: none; }`;
      document.head.append(styleTag);
    </script>
  </body>
</html>

Here is how the parsing process looks like:

This is not good, but it's not how most front-end libraries and frameworks handle CSS-in-JS.

Here's why:

  1. Our old code immediately created a <style> tag. But frameworks don't always work this fast.
  2. Your framework needs to set up the <App/> component first. This might not happen right away.
  3. The CSS-in-JS library might not add the CSS simultaneously as your component appears.

Because of these delays, you might see two "flashes of unstyled content" (FOUCs):

  • First, when the HTML loads without any framework content
  • Second, when the framework content appears but the CSS isn't ready yet

Compiled CSS

Not all CSS-in-JS tools are slow. Some newer tools like StyleX or PandaCSS don't have the same speed issues we mentioned above.

But why?

These tools dodge the JavaScript slowdown by using a clever trick: they turn your CSS-in-JS into regular CSS before your site even loads in a browser.

For example, let's say you write some Stylex code:

import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
  container: {
    backgroundColor: 'var(--red-600)',
  },
});

export function App() {
  return <div {...stylex.props(styles.container)} />;
}

When you build your site, it changes that code into normal CSS right on your computer:

<!DOCTYPE html>
<html lang="en">
    <head>
        <style>
        .bg_red\.600 {
            background: var(--colors-red-600);
        }
        </style>
    </head>
    <body>
        <!-- ... -->
    </body>
</html>

So when someone visits your site, the CSS is already there - no waiting for JavaScript.

Basically, we go from this:

to this:

It's worth noting that the line between "slow" and "fast" CSS-in-JS isn't always clear-cut.

Some of the tools we called "slow" earlier have ways to speed things up, like special plugins or tricks that work with server-side rendering (SSR).

Now you have a better idea of why some CSS-in-JS are faster than others, and what to consider when choosing one for your project.