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 <IconButton />
would extend the <BaseButton />
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 <App />
component, two different React trees got rendered. The outputted trees can be seen in the screenshots below:
The React tree using a normal div
The React tree using a styled.div
element
I then forced a re-render of the <App />
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?
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 <style>
tags in the document whenever a component mounts and/or has its props changed. These style tags normally contain a single CSS class, whose hashed name is mapped to a single React component. Whenever this component’s props change, the associated <style>
tag must change as well. This is done by re-evaluating the CSS rules that the style tag needs to have, creating a new hashed class name to hold the aforementioned CSS rules and updating the classname
prop of the associated React component in order to point to the recently-created class.
Let’s take for example the styled-components library. Whenever you create a styled.div
, the library assigns an internal ID to this component and adds an empty <style>
tag to the HTML <head>
. This tag contains a single comment that references the internal ID of the React component that’s related to it:
<style data-styled-components> /* sc-component-id: sc-bdVaJa */ </style>
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
<style>
tag in the HTML<head>
.
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 <style>
tag from within the React component, another Context.Consumer
is needed to provide access to the stylesheet instance. That’s why we see those two (2) Consumers in most CSS-in-JS libraries.
In addition, because these computations will affect the UI, they have to be performed during the render phase of the component and cannot be performed as a React lifecycle side-effect (since they would be delayed and perceived by the user as lag). This is why the rendering takes longer in a simple styled.div
than in a native one.
Now, the styled-components maintainers noticed that and added optimizations and early bailout techniques in order to bring down the time it takes for a component to re-render. Specifically, the library checks to see whether your styled component is “static”, meaning that its styling doesn’t depend on a theme or the component’s passed props. For example, the following component is static:
const StaticStyledDiv = styled.div` color:red `;
while this isn’t:
const DynamicStyledDiv = styled.div` color: ${props => props.color} `;
If the library detects a static component, it will skip steps 1- 4, since it can understand that the generated class name will never have to change (since there is no dynamic element to modify its related CSS rules). In addition, it won’t render a ThemeContext.Consumer
around the styled component, since a theme dependence would have prevented the component from being “static” in the first place.
If you were really observant, you would have noticed that even in production mode, the screenshot above rendered 2 Context.Consumer
components for each styled.div
. Interestingly though, the component that got rendered was “static” since it didn’t have any dynamic CSS rules and we would expect styled-components to skip the Consumer
that had to do with the theme. The reason you see 2 Consumer
s per component, is because the screenshots above were taken while utilizing emotion, another CSS-in-JS library. This library follows a similar approach, with minor differences. Again, it parses the tagged template, preprocesses it with stylis and updates the corresponding style
tag. One key difference is that emotion always wraps all components with a ThemeContext.Consumer
regardless of whether they are using a theme or not (which explains the screenshots above). Funnily enough, even though it renders more consumer components, it still outperforms styled-components, which denotes that the number of consumer isn’t the biggest contributor to a slow render. It should be noted that at the time of writing there is a beta version for the v5.x.x of styled-components, which will outperform emotion according to its maintainers.
So, to wrap up, the combination of multiple Context
consumers (which means additional elements that React has to coordinate) and the inherent housekeeping that dynamic styling goes with, may be slowing down your app. It should also be mentioned, that the all the style
tags that get added for each component never get removed at all. This is because the overhead associated with a DOM removal (e.g. browser reflows) is higher than the overhead of just keeping them there. To be honest, I’m unsure whether dangling style
tags can create performance issues, since they only contain unused classes that are generated during runtime (e.g. don’t get shipped over the wire), but it’s something you should potentially consider for your app.
To be fair, those style
tags are not created by all CSS-in-JS libraries, since not all of them are runtime-based. For example, linaria is a zero-runtime CSS-in-JS library that defines a set of fixed CSS classes during build time and maps all dynamic rules within a tagged template (i.e. CSS rules that depend on prop values) with CSS custom properties. Thus whenever a props changes, the CSS custom property changes and the UI updates. This makes it much faster than all runtime-based CSS-in-JS libraries, since the amount of work and housekeeping that needs to be done during a render is much less. Realistically, the only thing that it needs to do during render is to make sure to — potentially — update a CSS custom property. At the same time though, it’s not compatible with IE11, has limited support for the popular css
prop and doesn’t offer theming capabilities out of the box. As with most libraries, there is no silver bullet.
Takeaways
CSS-in-JS was a revolutionary pattern which brought a better experience for many developers out there, while also solving many issues such as name collisions, vendor prefixing, etc. out of the box. The point of this article was to shed some light into the potentially unknown performance implications when using the most prominent CSS-in-JS libraries (e.g. the ones with a runtime). I want to stress that those perf considerations don’t always create problems for an app. In fact, most apps won’t even notice them unless they are relying on hundreds of concurrently rendered composed components. The benefits of CSS-in-JS typically outweigh the aforementioned perf implications, but these implications are something that developers of applications with tons of data and lots of rapidly changing UIs should definitely consider. Before you jump on any refactoring train though, please measure and judge for yourselves.
Finally, here are some techniques that you can employ in order to increase your app’s performance when using one of the popular runtime-based CSS-in-JS libraries:
- Don’t over-compose styled components
Basically don’t do what I did and try to compose 3 individual styled instances, just to create a freaking button. If you want to “share” code, make use of thecss
prop and compose tagged templates. This will save you lots of unneededContext
consumers, which means fewer components for React to manage, which means that React’s runtime can do its work faster. - Prefer “static” components
Some CSS-in-JS libraries will optimize their execution when your CSS has no dependencies on theme or props. The more “static” your tagged templates are, the higher the chances that your CSS-in-JS runtime will execute faster. - Avoid unneeded React re-renders
Make sure to only render when you need to, so you can avoid work by both React’s and CSS-in-JS library’s runtimes. Realistically, that should only be needed in extreme scenarios where a lot of heavy components are being simultaneously rendered on the screen. - Investigate whether a zero-runtime CSS-in-JS library can work for your project
Sometimes we tend to prefer writing CSS in JS for the DX (developer experience) it offers, without a need to have access to an extended JS API. If you app doesn’t need support for theming and doesn’t make heavy and complex use of thecss
prop, then a zero-runtime CSS-in-JS library might be a good candidate. As a bonus you will shave ~12KB off your overall bundle size, since most CSS-in-JS libraries range between 10KB — 15KB, while zero-runtime ones (like linaria) are < 1KB.
That’s it! Thanks a lot for reading 🙂
P.S. If you’ve ever wondered why the CSS rules are not editable by the devtools inspector, it’s because they make use of CSSStyleSheet.insertRule()
. This is a really performant way of modifying a stylesheet, but one of its downsides is that the associated stylesheet is no longer editable through the inspector.