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.
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…
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
The React tree using a
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.54ms
Development 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.06ms
Production 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?
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:
- Parses the styled component’s tagged template’s CSS rules.
- Generates the new CSS class name (or checks whether it should retain the existing one).
- Preprocesses the styles with stylis.
- Injects the preprocessed CSS into the associated
tag in the HTML
To be able to use the theme during step (1), a
Context.Consumer is needed in order to read the theme’s values within the tagged template. In order to be able to be able to modify the associated