DEV Community

Sam Magura
Sam Magura

Posted on • Updated on

Why We're Breaking Up with CSS-in-JS

Hi, I'm Sam — software engineer at Spot and the 2nd most active maintainer of Emotion, a widely-popular CSS-in-JS library for React. This post will delve into what originally attracted me to CSS-in-JS, and why I (along with the rest of the Spot team) have decided to shift away from it.

We'll start with an overview of CSS-in-JS and give an overview of its pros & cons. Then, we'll do a deep dive into the performance issues that CSS-in-JS caused at Spot and how you can avoid them.

What is CSS-in-JS?

As the name suggests, CSS-in-JS allows you to style your React components by writing CSS directly in your JavaScript or TypeScript code:

// @emotion/react (css prop), with object styles
function ErrorMessage({ children }) {
  return (
    <div
      css={{
        color: 'red',
        fontWeight: 'bold',
      }}
    >
      {children}
    </div>
  );
}

// styled-components or @emotion/styled, with string styles
const ErrorMessage = styled.div`
  color: red;
  font-weight: bold;
`;
Enter fullscreen mode Exit fullscreen mode

styled-components and Emotion are the most popular CSS-in-JS libraries in the React community. While I have only used Emotion, I believe virtually all points in this article apply to styled-components as well.

This article focuses on runtime CSS-in-JS, a category which includes both styled-components and Emotion. Runtime CSS-in-JS simply means that the library interprets and applies your styles when the application runs. We'll briefly discuss compile-time CSS-in-JS at the end of the article.

The Good, The Bad, and the Ugly of CSS-in-JS

Before we get into the nitty-gritty of specific CSS-in-JS coding patterns and their implications for performance, let's start with a high-level overview of why you might choose to adopt the technology, and why you might not.

The Good

1. Locally-scoped styles. When writing plain CSS, it's very easy to accidentally apply styles more widely than you intended. For example, imagine you're making a list view where each row should have some padding and a border. You'd likely write CSS like this:

   .row {
     padding: 0.5rem;
     border: 1px solid #ddd;
   }
Enter fullscreen mode Exit fullscreen mode

Several months later when you've completely forgotten about the list view, you create another component that has rows. Naturally, you set className="row" on these elements. Now the new component's rows have an unsightly border and you have no idea why! While this type of problem can be solved by using longer class names or more specific selectors, it's still on you as the developer to ensure there are no class name conflicts.

CSS-in-JS completely solves this problem by making styles locally-scoped by default. If you were to write your list view row as

<div css={{ padding: '0.5rem', border: '1px solid #ddd' }}>...</div>
Enter fullscreen mode Exit fullscreen mode

there is no way the padding and border can accidentally get applied to unrelated elements.

Note: CSS Modules also provide locally-scoped styles.

2. Colocation. If using plain CSS, you might put all of your .css files in a src/styles directory, while all of your React components live in src/components. As the size of the application grows, it quickly becomes difficult to tell which styles are used by each component. Often times, you will end up with dead code in your CSS because there's no easy way to tell that the styles aren't being used.

A better approach for organizing your code is to include everything related to a single component in same place. This practice, called colocation, has been covered in an excellent blog post by Kent C. Dodds.

The problem is that it's hard to implement colocation when using plain CSS, since CSS and JavaScript have to go in separate files, and your styles will apply globally regardless of where the .css file is located. On the other hand, if you're using CSS-in-JS, you can write your styles directly inside the React component that uses them! If done correctly, this greatly improves the maintainability of your application.

Note: CSS Modules also allow you to colocate styles with components, though not in the same file.

3. You can use JavaScript variables in styles. CSS-in-JS enables you to reference JavaScript variables in your style rules, e.g.:

// colors.ts
export const colors = {
  primary: '#0d6efd',
  border: '#ddd',
  /* ... */
};

// MyComponent.tsx
function MyComponent({ fontSize }) {
  return (
    <p
      css={{
        color: colors.primary,
        fontSize,
        border: `1px solid ${colors.border}`,
      }}
    >
      ...
    </p>
  );
}
Enter fullscreen mode Exit fullscreen mode

As this example shows, you can use both JavaScript constants (e.g. colors) and React props / state (e.g. fontSize) in CSS-in-JS styles. The ability to use JavaScript constants in styles reduces duplication in some cases, since the same constant does not have to be defined as both a CSS variable and a JavaScript constant. The ability to use props & state allows you to create components with highly-customizable styles, without using inline styles. (Inline styles are not ideal for performance when the same styles are applied to many elements.)

The Neutral

1. It's the hot new technology. Many web developers, myself included, are quick to adopt the hottest new trends in the JavaScript community. Part of this is rationale, since in many cases, new libraries and frameworks have proven to be massive improvements over their predecessors (just think about how much React enhances productivity over earlier libraries like jQuery). On the other hand, the other part of our obsession with shiny new tools is just that — an obsession. We're afraid of missing out on the next big thing, and we might overlook real drawbacks when deciding to adopt a new library or framework. I think this has certainly been a factor in the widespread adoption of CSS-in-JS — at least it was for me.

The Bad

1. CSS-in-JS adds runtime overhead. When your components render, the CSS-in-JS library must "serialize" your styles into plain CSS that can be inserted into the document. It's clear that this takes up extra CPU cycles, but is it enough to have a noticeable impact on the performance of your application? We'll investigate this question in depth in the next section.

2. CSS-in-JS increases your bundle size. This is an obvious one — each user who visits your site now has to download the JavaScript for the CSS-in-JS library. Emotion is 7.9 kB minzipped and styled-components is 12.7 kB. So neither library is huge, but it all adds up. (react + react-dom is 44.5 kB for comparison.)

3. CSS-in-JS clutters the React DevTools. For each element that uses the css prop, Emotion will render <EmotionCssPropInternal> and <Insertion> components. If you are using the css prop on many elements, Emotion's internal components can really clutter up the React DevTools, as seen here:

The React DevTools displaying many internal Emotion components

The Ugly

1. Frequently inserting CSS rules forces the browser to do a lot of extra work. Sebastian Markbåge, member of the React core team and the original designer of React Hooks, wrote an extremely informative discussion in the React 18 working group about how CSS-in-JS libraries would need to change to work with React 18, and about the future of runtime CSS-in-JS in general. In particular, he says:

In concurrent rendering, React can yield to the browser between renders. If you insert a new rule in a component, then React yields, the browser then have to see if those rules would apply to the existing tree. So it recalculates the style rules. Then React renders the next component, and then that component discovers a new rule and it happens again.

This effectively causes a recalculation of all CSS rules against all DOM nodes every frame while React is rendering. This is VERY slow.

Update 2022-10-25: This quote from Sebastian is specifically referring to performance in React Concurrent Mode, without useInsertionEffect. I recommend reading the full discussion if you want an in-depth understanding of this. Thanks to Dan Abramov for pointing out this inaccuracy on Twitter.

The worst thing about this problem is that it's not a fixable issue (within the context of runtime CSS-in-JS). Runtime CSS-in-JS libraries work by inserting new style rules when components render, and this is bad for performance on a fundamental level.

2. With CSS-in-JS, there's a lot more that can go wrong, especially when using SSR and/or component libraries. In the Emotion GitHub repository, we receive tons of issues that go like this:

I'm using Emotion with server-side rendering and MUI/Mantine/(another Emotion-powered component library) and it's not working because...

While the root cause varies from issue to issue, there are some common themes:

  • Multiple instances of Emotion get loaded at once. This can cause problems even if the multiple instances are all the same version of Emotion. (Example issue)
  • Component libraries often do not give you full control over the order in which styles are inserted. (Example issue)
  • Emotion's SSR support works differently between React 17 and React 18. This was necessary for compatibility with React 18's streaming SSR. (Example issue)

And believe me, these sources of complexity are just the tip of the iceberg. (If you're feeling brave, take a look at the TypeScript definitions for @emotion/styled.)

Performance Deep Dive

At this point, it's clear that there are both significant pros and significant cons to runtime CSS-in-JS. To understand why our team is moving away from the technology, we need to explore the real-world performance impact of CSS-in-JS.

This section focuses on the performance impact of Emotion, as it was used in the Spot codebase. As such, it would be a mistake to assume that the performance numbers presented below apply to your codebase as well — there are many ways to use Emotion, and each of these has its own performance characteristics.

Serialization Inside of Render vs. Outside of Render

Style serialization refers to the process by which Emotion takes your CSS string or object styles and converts them to a plain CSS string that can be inserted into the document. Emotion also computes a hash of the plain CSS during serialization — this hash is what you see in the generated class names, e.g. css-15nl2r3.

While I have not measured this, I believe one of the most significant factors in how Emotion performs is whether style serialization is performed inside or outside of the React render cycle.

The examples in the Emotion docs perform serialization inside render, like this:

function MyComponent() {
  return (
    <div
      css={{
        backgroundColor: 'blue',
        width: 100,
        height: 100,
      }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Every time MyComponent renders, the object styles are serialized again. If MyComponent renders frequently (e.g. on every keystroke), the repeated serialization may have a high performance cost.

A more performant approach is to move the styles outside of the component, so that serialization happens one time when the module loads, instead of on each render. You can do this with the css function from @emotion/react:

const myCss = css({
  backgroundColor: 'blue',
  width: 100,
  height: 100,
});

function MyComponent() {
  return <div css={myCss} />;
}
Enter fullscreen mode Exit fullscreen mode

Of course, this prevents you from accessing props in your styles, so you are missing out on one of the main selling points of CSS-in-JS.

At Spot, we performed style serialization in render, so the following performance analysis will focus on this case.

Benchmarking the Member Browser

It's finally time to make things concrete by profiling a real component from Spot. We'll be using the Member Browser, a fairly simple list view that shows you all of the users in your team. Virtually all of the Member Browser's styles are using Emotion, specifically the css prop.

The Member Browser in Spot

For the test,

  • The Member Browser will display 20 users,
  • The React.memo around the list items will be removed, and
  • We'll force the top-most <BrowseMembers> component to render each second, and record the times for the first 10 renders.
  • React Strict Mode is off. (It effectively doubles the render times you see in the profiler.)

I profiled the page using the React DevTools and got 54.3 ms as the average of the first 10 render times.

My personal rule of thumb is that a React component should take 16 ms or less to render, since 1 frame at 60 frames per second is 16.67 ms. The Member Browser is currently over 3 times this figure, so it's a pretty heavyweight component.

This test was performed on an M1 Max CPU which is WAY faster than what the average user will have. The 54.3 ms render time that I got could easily be 200 ms on a less powerful machine.

Analyzing the Flamegraph

Here's the flamegraph for a single list item from the above test:

Performance flamegraph of the BrowseMembersItem component

As you can see, there are a huge number of <Box> and <Flex> components being rendered — these are our "style primitives" which use the css prop. While each <Box> only takes 0.1 – 0.2 ms to render, this adds up because the total number of <Box> components is massive.

Benchmarking the Member Browser, without Emotion

To see how much of this expensive render was due to Emotion, I rewrote the Member Browser styles using Sass Modules instead of Emotion. (Sass Modules are compiled to plain CSS at build time, so there is virtually no performance penalty to using them.)

I repeated the same test described above and got 27.7 ms as the average of the first 10 renders. That's a 48% decrease from the original time!

So, that's the reason we are breaking up with CSS-in-JS: the runtime performance cost is simply too high.

To repeat my disclaimer from above: this result only directly applies to the Spot codebase and how we were using Emotion. If your codebase is using Emotion in a more performant way (e.g. style serialization outside of render), you will likely see a much smaller benefit after removing CSS-in-JS from the equation.

Here is the raw data for those who are curious:

Spreadsheet showing render times between Emotion and non-Emotion Member Browser

Our New Styling System

After we made up our minds to switch away from CSS-in-JS, the obvious question is: what should we be using instead? Ideally, we want a styling system that has performance similar to that of plain CSS while keeping as many of the benefits of CSS-in-JS as possible. Here are the primary benefits of CSS-in-JS that I described in the section titled "The Good":

  1. Styles are locally-scoped.
  2. Styles are colocated with the components they apply to.
  3. You can use JavaScript variables in styles.

If you paid close attention to that section, you'll remember that I said that CSS Modules also provide locally-scoped styles and colocation. And CSS Modules compile to plain CSS files, so there is no runtime performance cost to using them.

The main downside to CSS Modules in my mind is that, at end of the day, they are still plain CSS — and plain CSS is lacking features that improve DX and reduce code duplication. While nested selectors are coming to CSS, they aren't here yet, and this feature is a huge quality of life boost for us.

Fortunately, there is an easy solution to this problem — Sass Modules, which are simply CSS Modules written in Sass. You get the locally-scoped styles of CSS Modules AND the powerful build-time features of Sass, with essentially no runtime cost. This is why Sass Modules will be our general purpose styling solution going forward.

Side note: With Sass Modules, you lose benefit 3 of CSS-in-JS (the ability to use JavaScript variables in styles). Though, you can use an :export block in your Sass file to make constants from the Sass code available to JavaScript. This isn't as convenient, but it keeps things DRY.

Utility Classes

One concern the team had about switching from Emotion to Sass Modules is that it would be less convenient to apply extremely common styles, like display: flex. Before, we would write:

<FlexH alignItems="center">...</FlexH>
Enter fullscreen mode Exit fullscreen mode

To do this using only Sass Modules, we would have to open the .module.scss file and create a class that applies the styles display: flex and align-items: center. It's not the end of the world, but it's definitely less convenient.

To improve the DX around this, we decided to bring in a utility class system. If you aren't familiar with utility classes, they are CSS classes that set a single CSS property on the element. Usually, you will combine multiple utility classes to get the desired styles. For the example above, you would write something like this:

<div className="d-flex align-items-center">...</div>
Enter fullscreen mode Exit fullscreen mode

Bootstrap and Tailwind are the most popular CSS frameworks that offer utility classes. These libraries have put a lot of design effort into their utility systems, so it made the most sense to adopt one of them instead of rolling our own. I had already been using Bootstrap for years, so we went with Bootstrap. While you can bring in the Bootstrap utility classes as a pre-built CSS file, we needed to customize the classes to fit our existing styling system, so I copied the relevant parts of the Bootstrap source code into our project.

We've been using Sass Modules and utility classes for new components for several weeks now and are quite happy with it. The DX is similar to that of Emotion, and the runtime performance is vastly superior.

Side note: We're also using the typed-scss-modules package to generate TypeScript definitions for our Sass Modules. Perhaps the largest benefit of this is that it allowed us to define a utils() helper function that works like classnames, except it only accepts valid utility class names as arguments.

A Note about Compile-Time CSS-in-JS

This article focused on runtime CSS-in-JS libraries like Emotion and styled-components. Recently, we've seen an increasing number of CSS-in-JS libraries that convert your styles to plain CSS at compile time. These include:

These libraries purport to provide a similar benefits to runtime CSS-in-JS, without the performance cost.

While I have not used any compile-time CSS-in-JS libraries myself, I still think they have drawbacks when compared with Sass Modules. Here are the drawbacks I saw when looking at Compiled in particular:

  • Styles are still inserted when a component mounts for the first time, which forces the browser to recalculate the styles on every DOM node. (This drawback was discussed in the section title "The Ugly".)
  • Dynamic styles like the color prop in this example cannot be extracted at build time, so Compiled adds the value as a CSS variable using the style prop (a.k.a. inline styles). Inline styles are known to cause suboptimal performance when applied many elements.
  • The library still inserts boilerplate components into your React tree as shown here. This will clutter up the React DevTools just like runtime CSS-in-JS.

Conclusion

Thanks for reading this deep dive into runtime CSS-in-JS. Like any technology, it has its pros and cons. Ultimately, it's up to you as a developer to evaluate these pros and cons and then make an informed decision about whether the technology is right for your use case. For us at Spot, the runtime performance cost of Emotion far outweighed the DX benefits, especially when you consider that the alternative of Sass Modules + utility classes still has a good DX while providing vastly superior performance.

About Spot

At Spot, we're building the future of remote work. When companies go remote, they often lose the sense of connection and culture that was present in the office. Spot is a next-gen communication platform that brings your team together by combining traditional messaging and video conferencing features with the ability to create & customize your own 3D virtual office. Please check us out if that sounds interesting!

P.S. We're looking for talented software engineers to join the team! See here for details.

A picture of Spot

This post was also published on the Spot blog.

Latest comments (138)

Collapse
 
nitipit profile image
Nitipit Nontasuwan • Edited

Hi, I'm from 2024 :)
CSS-in-JS is still the future ... with many features it can achieve.

  1. Component Styling in OOP manner. keenlycode.github.io/adapter/guide...
  2. Style under Shadow DOM just work with adoptedStyleSheet keenlycode.github.io/adapter/guide...
  3. Element Scoped Style with nesting rules. keenlycode.github.io/adapter/guide...
  4. Style Filtering keenlycode.github.io/adapter/guide...
  5. Web Assembly for fast parser : lightningcss.dev/

If we use cssStyleSheet, then CSS can be and should be parsed just once per page, not per component or per render.

The downside maybe it less compatible with old browsers (~90% compatible). However, it's possible to fixed it with polyfills or JS Build Tools to target for older browsers.

and much more...

If you're curious , you can visit Adapter

Collapse
 
gameboyzone profile image
Hardik Shah

Hello @srmagura - this post is amazing and covers several aspects. Thank you for taking time to explain the pain-points and discussing several view points.

A question for @srmagura and others knowledgeable -

I am using EmotionJS in my React component library wherein every component has it's corresponding EmotionJS styles, how can I migrate my EmotionJS styles to SASS style file (i.e. .scss) in an automated fashion if I want to break away with EmotionJS?

This is precisely the topic of this article but I didn't see any concrete plan for automated migration.

Collapse
 
albanx0 profile image
Alban X

Who ever invented or uses CSS-in-JS deserves a special place in hell

Collapse
 
gustavo_magnago_b5430810e profile image
Gustavo Magnago

Idk, for me even after this I will keep Emotion, it’s just easier to code, cleaner and, specially for a high ADHD person as I, it gets beautifully organised with colocation, making it way easier to maintain and read through the years working at the same project. Theming is my passion ❤️

Collapse
 
nerdydeedsllc profile image
Nerdy Deeds, LLC

The pushback OP is experiencing here honestly baffles me. Frankly, so does that amount of struggle folk seem to be having with the simple act of being expected to scope your bloody stylesheets! This is something I expect even my JD's to be proficient in within 2 weeks of their first hire! Hell, we have INTERNS here that have no troubles with the concept after a 90 second explanation!

There are literally DOZENS of ways to address this, many (most?) that require a minuscule amount of thought: so much so that it's wholly possible to establish up-front, with the inclusion of a couple lines of React code and a SINGLE line of Sass code per page/view or component!

My ghast is well and truly flabbered that, for what certainly appears to have been a solved issue for years now, there's this much controversy surrounding it, based almost exclusively, based on the comments here, at least, on the arguments, "but it's nominally-less convenient!" and "but we don't WANNA!".

The ability to use JS variables inside of CSS was, briefly, moderately handy (although in the end, it's still transpiled down to CSS! Meaning that there's an extremely finite number of use-cases not already solved by CSS itself - or certainly by Sass for the remainder - nowadays).

But guys? It. Is. Not. Performant!

And this is coming from an engineer from one of the TOP TWO AUTHORS OF THE PRODUCT HE IS DESCRIBING!

Now, maybe it's because I'm old, and remember having to optimize down to the BYTE for load-, transfer-, and render-times (hell, I worked a web project where we optimized for the distance the drive head had to move on the disk!), but, to me, a 50-200% savings in load time simply by moving the same code to a different file (which is INTENDED for the purpose), and following basic conventions (which, a damn linter can handle for you!) is a bloody no-brainer non-starter. The problem is NOT that your server/users' computers aren't powerful enough. It's that you're being LAZY. "I don't wanna" is not a valid excuse for sloppy coding or anti-patterns!

Sorry! Learn the language and get over having everything auto-scripted for you! Otherwise, I'm certain there's a GPT out there that CAN handle the additional complexity of ONE line of code.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

and your styles will apply globally regardless of where the .css file is located

This can be somewhat mitigated by having the JS link the corresponding CSS file like so:

// The made-up function that does the magic:
linkStyles('/path/to/styles.css', { scope: 'my-component' })
// Defining the component in some way:
class MyComponent { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

This would allow putting the CSS in a separate file, either in a parallel tree or next to the JS file, lets server-side tools include preloading tags in the resulting CSS, allows the browser to fetch and cache styling separately from the JS, and otherwise comes with most of the same maintainability advantages as CSS-in-JS.

Collapse
 
adityatyagi profile image
Aditya Tyagi

Came here from Shifting Away From CSS-in-JS With Sam Magura - RRU 211 (react round up). If anyone here hasn't, please check it out: topenddevs.com/podcasts/react-roun...

Collapse
 
zicjin profile image
zicjin • Edited

I understand the importance of performance, but the development efficiency is also very important (especially toB projects). Is there any rich UI library based on scss besides the old bootstrap? Almost every sleek, beautiful new UI library I see is built with css-in-js libraries.

Collapse
 
hamzahamidi profile image
Hamza Hamidi

Styled-components are a major source of performance issues with re-renders and Js execution time especially in mobile devices. If your code is SEO vital, styled components is going to harm your Core Web Vitals. You are much better off with CSS Modules.

Collapse
 
hamzahamidi profile image
Hamza Hamidi

The idea was doomed from the start, JS performance on mobile devices is very poor especially on Android devices. You're better off by using vanilla css modules. Don't forget that Google crawlers use the mobile view to benchmark your website! Not surprising since 70% of web traffic are from mobile devices, and this number will continue to grow.

Collapse
 
stephenh profile image
Stephen Haberman

@srmagura curious if you've published the benchmark code? Would be fun to play around with. Thanks!

Collapse
 
nitipit profile image
Nitipit Nontasuwan

I think CSS-in-JS is not a problem here, It's about how you use CSS-in-JS with React.

 
xcmk123 profile image
xcmk123

@jfbrennan your demo github.com/jfbrennan/m- is really awesome. Is there any way to use that on ReactJS project ?

Collapse
 
lil5 profile image
Lucian I. Last

Here’s a quick converter I built

lil5.github.io/jss-to-scss/

 
jasper91 profile image
Jasperrr91

Interesting bits of code to browse through and it certainly has its charm. I would however never implement such code in companies I work at. It's far harder to read than simple components in . Making it harder to maintain since with the current job market there's mainly juniors and medior developers joining companies. The main key is to keep your code as simple and maintainable as possible.

Collapse
 
vadorequest profile image
Vadorequest

I'm aware of the perf issues with Emotion, and still chose it because I didn't find a better alternative to get dynamic CSS based on runtime variables coming from an API.