• Home / css-in-js / Web Performance Calendar…

Web Performance Calendar » The unseen performance costs of modern CSS-in-JS libraries in React apps

CSS-in-JS is becoming a popular choice for any new front-end app out there, due to the fact that it offers a better API for developers to work with. Don’t get me wrong, I love CSS, but creating a proper CSS architecture is not an easy task. Unfortunately though, besides some of the great advantages CSS-in-JS boasts over traditional CSS, it may still create performance issues in certain apps. In this article, I will attempt to demystify the high-level strategies of the most popular CSS-in-JS libraries, discuss the performance issues they may introduce on occasion and finally consider techniques that we can employ to mitigate them. So, without further ado, let’s jump straight in.

Background

In my company we figured it would be useful to build a UI library in order to be able to re-use common UI pieces across different products and I was the one to volunteer to get this endeavor started. I chose to use a CSS-in-JS solution, since I was already really happy with the styled API that most of the popular libraries expose. As I was developing it, I wanted to be smart and have re-usable logic and shared props across my components, so I started composing them. For example, an would extend the that in turn implements a simple styled.button. Unfortunately, the IconButton needed to have its own styling, so it was converted to a styled component along the lines of:

const IconButton = styled(BaseButton)`
  border-radius: 3px;
`;

As more and more components were added, more and more compositions took place and it didn’t feel awkward since React was built upon the concepts of this very notion. Everything was fine until I implemented a Table. I started noticing that the rendering felt slow, especially when the number of rows got more than 50. Thus, I opened my devtools to try and investigate it.

Well needless to say, the React tree was as big as Jack’s magical beanstalk. The amount of Context.Consumer components was so high, that it could easily keep me up at nights. You see, each time you render a single styled component using styled-components or emotion, apart from the obvious React Component that gets created, an additional Context.Consumer is added in order to allow the runtime script (that most CSS-in-JS libraries depend upon) to properly manage the generated styling rules. This normally shouldn’t be too much of a problem, but don’t forget that components need to have access to your theme. This translates to an additional Context.Consumer being rendered for each styled element in order to “read” the theme from the ThemeProvider component. All in all, when you create a styled component in an app with a theme, 3 components get created: the obvious StyledXXX component and two (2) additional consumer components. Don’t be too scared, React does its work fast and this won’t be too much of an issue most of the times, but what if we compose multiple styled components in order to create a more complex component? What if this complex component is part of a big list or a table, where at least 100 of those get rendered? That’s when problems arise…

Profiling

To test CSS-in-JS solutions I created the simplest of apps, which just renders 50 “Hello World” statements. On the first experiment, I wrapped the text in a traditional div element, while on the second one, I utilized a styled.div component instead. I also added a button that would force a react re-render on those 50 div elements whenever it was clicked. The code for both can be seen on the following gists:

After rendering the component, two different React trees got rendered. The outputted trees can be seen in the screenshots below:

The React tree using a normal divThe React tree using a normal div

The React tree using a styled.div elementThe React tree using a styled.div element

I then forced a re-render of the 10 times in order to gather some metrics with regards to the perf costs that these additional Context.Consumer components bring. The timings of the re-renders in development modecan be seen below:

Development render timings for simple div, Average: 2.54msDevelopment render timings for simple div. Average: 2.54ms

Development render timings for styled.div. Average: 3.98msDevelopment render timings for styled.div . Average: 3.98ms

So interestingly enough, on average, the CSS-in-JS implementation is 56.6% more expensive in this example. Let’s see if things are different in production mode. The timings of the re-renders in production mode can be seen below:

Production render timings for simple div, Average 1.06msProduction render timings for simple div. Average 1.06ms

Production render timings for styled.div. Average 2.27msProduction render timings for styled.div. Average 2.27ms

When production mode is on, the implementation with the simple div seems to benefit the most by dropping its rendering time by more than 50% compared to a 43%drop on the CSS-in-JS implementation. Still, the latter takes almost twice as much time to render than the former. So what exactly is it that makes it slower?

Runtime Analysis

The obvious answer would be “Erm… you just said CSS-in-JS libraries render two Context.Consumer per component”, but if you really think about it, a context consumer is nothing more than accessing a JS variable. Sure, React has to do its work to figure out where to read the value from, but that alone doesn’t justify the timings above. The real answer comes from analyzing the reason why those contexts exist in the first place. You see, most CSS-in-JS libraries depend on a runtime that helps them dynamically update the styles of a component. These CSS-in-JS libraries don’t create CSS classes at build-time, but instead dynamically generate and update

When the associated React component gets rendered, styled-components:

  1. Parses the styled component’s tagged template’s CSS rules.
  2. Generates the new CSS class name (or checks whether it should retain the existing one).
  3. Preprocesses the styles with stylis.
  4. Injects the preprocessed CSS into the associated