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;
`;
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;
}
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>
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>
);
}
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 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,
}}
/>
);
}
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} />;
}
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.
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:
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:
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":
- Styles are locally-scoped.
- Styles are colocated with the components they apply to.
- 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>
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>
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 thestyle
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.
This post was also published on the Spot blog.
Top comments (140)
To be honest, the whole "it's hard to tell what's used by what" in plain CSS is already solved with BEM.
BEM scales very nicely.
So by using new CSS variables with CSS modules to create locally scoped overrides you can still create runtime themed styles.
Yes your "frontend" Devs will need to learn CSS instead of just treating it like the Typescript autocomplete object faceroll experience that current cssinjs promotes.
Other unpopular opinions I hold:
Nested CSS rules made popular by lesscss are a maintenance nightmare. Don't use them.
Extends is also bad.
Mixins... Yep I hate them too.
I love BEM and it's what I use when writing regular CSS for my own use.
The problem with BEM isn't BEM itself, it's getting your coworkers, new developers
and external agencies to understand and follow the syntax. We implemented BEM at a previous workplace and it worked perfectly until some core developers quit and new ones were hired, and then the styles just quickly deteriorated.
CSS modules and CSS-in-JS are locally scoped by default, and you have to specifically add syntax to make things global. As much as I dislike Tailwind syntax, that too has the advantage of keeping styling local to the place it's applied at least.
This is my experience as well. BEM can scale really well, but keeping it consistent is a problem when you have multiple people working in a codebase. I mean same can be said for any tool. Getting everyone on board is usually the hardest part.
People are also detracted from BEM because of it's syntax and because it "looks ugly" to them, but once you actually start using it, your CSS becomes a breeze to use and read because its "standardized" in some way. It also produces flat styling without horrible nesting which is a big plus in my book. Simpler flat styling allows easier per situation override if needed and you don't have to battle CSS selector nesting and specificity that much at all.
When using BEM, I've usually went with BEM + utility classes approach. Some more common things like spacing (padding, margin), colors, and maybe some flex/block properties would be mostly in utility classes and everything else would be BEM. It worked quite nicely for projects that I've been working on. It also allowed me to build UI faster because I wouldn't have to apply same spacing values all over again for each component, but could just apply a utility class name to some wrapper element and get the spacing that I want.
These days when working with React, the projects that I usually work on use SASS Modules and that also scales nicely. It's easier to get people on the same page because there is no specific naming convention like BEM and people don't have to worry about naming things because everything is locally scoped. This approach works nicely in most situations except few edge cases where styles need to be shared or something kinda needs to be global, then it's not so elegant anymore and in a single file. But other than that, it works really well and I don't have to leave comments on PRs like "hey the name of this class doesn't adhere to BEM principles, it should be this and that etc."
I still prefer SASS Modules compared to CSS-in-JS (emotion, styled-components) because you are still "mostly" writing just regular CSS that is closer to the native web.
I'm also working on a project that uses styled-components and even though I do like type safety we get with that because we use Typescript, it also makes every little styling its own component and to see any conditional logic for styling I always have to jump to source of a styled component to see what's going on. At least with SASS Modules and conditionally applying classNames I can immediately see what class is being applied for a certain condition and just by its name I can probably tell what it does without having to go into its source.
But that's just me and my personal experience.
When you refer to Sass Modules are you referring to this. How does that relieve you of having a specific naming convention like BEM? I use the new module system extensively but I still find the need for BEM. For e.g. I'm using the module system and BEM here. To me if in one module (say the button module) you have a class named
.primary
and in another module (say the link module) you also have a class named.primary
then those classes will class with each other once the Sass is compiled to CSS. Can you elaborate more on that?Sure. I'm referring to CSS Modules, but just using SASS files because I still love SASS nesting features github.com/css-modules/css-modules
It means you can import style files directly into the component file and classes will he hashed in some way to make them unique. That way you can have classes like
.primary
multiple times, but they won't clash because they are locally scoped by name.Did the new developers read the BEM documentation? In my experience a lot of people find that BEM "looks ugly" at first sight and then don't even try to understand it. While it solves so many problems.
There's linting you can use to enforce this.
Same (sorta), I will forever be in love with SASS and BE (BEM without the modifiers).
I satisfy all 3 of of the mentioned pro's of CSS in JS with: SASS+BE, custom HTML elements, and CSS variables.
I approach everything as a component and use custom HTML elements with a corresponding sass file with mixins only used to tap into our global design systems like our grids/buttons/fonts.
To me using BEM's modifier classes are equally as ugly (and annoying
.block__element--modifier???
blegh!) as using utility classes and I absolutely hate referencing them in my JavaScript files. I restrict the use of modifiers for side-effects of user interactions which are modified via JS so I use custom HTML attributes as modifiers instead and do not worry about it bleeding into other components since I am taking advantage of SASS's locally scoped nature. I also keep SASS Elements one level deep in terms of nesting. My SASS components are usually flat and I only nest when doing something like the lobotomized owl.For getting colors or dimensions from JS into the stylesheet, i inject custom css variables into the DOM element's style attribute and reference it in the css:
and in your SASS:
Works beautifully.
Another thing I would mention is that your nesting of media queries is also a thing i loathe.
the pattern I follow is :
thing/index.scss
This way, you're following a mobile first approach and then you don't end up with this :
It's a pretty simple example, but I've seen worse.
Nesting media queries is one of the reasons I still love and use SASS. It allows me to immediately see all behavior for a single piece of styling. I don't have to go into 3-4 files (like in your case) in order to see how this component behaves on each resolution.
But to each their own, we all have our own preferences when it comes to some of this stuff. To me personally it's just easier to piece all of this stuff together when it's all in the same file, I can just immediately see "Oh this selector behaves like this on mobile, after 'md' and after 'lg' breakpoints".
same! multiple sass files for a single component? seems like a step backwards to me, harder to see the whole picture.
With this pattern you don't know how a component behaves on different breakpoints. If the component is complex is even more so, since when you change one element you have to check 4 files to make sure that it works as you intended. I highly discourage this pattern, nested media queries seems the natural way of styling a component IMHO.
Indeed, it's much easier to work with media queries that are simply called whenever they needed on a small bit of styling; than splitting entire stylesheets based on those same media queries.
Yes, you might up with a few more media queries but at least it's very easy for each developer to see what's going on. Having to look into separate files for the styling of a single component, as well as remembering what's happening for every single viewport, is a tremendous pain and unscalable nor maintainable.
also, they implied that we aren't doing mobile first using this approach which we 100% are and it's very easy to see that we are.
if you are a stickler for order then you would start from the bottom up and declare the padding at the mobile level and later reset for wider devices
but I also don't mind the convenience of using
include-media
to specifically target mobile if I don't want to use the bottom up approach. it isn't hard at all to reason about and I use it sparingly, only when I know it's going to be mostly mobile only styles.I don't care about things being ugly.
My main goal is scaling in large teams and maintenance long term, so I care more about understanding dependencies and avoiding the cascade.
Looks like you've also dived deep into the problem 👍️
i guess it's the designer in me, but, to me, if it ain't beautiful, it ain't worth looking at.
Hello @eballeste ,
Few questions on your implementation -
Are you using custom elements to create your own Web components or using customized custom elementsi.e. class MyButton extends HTMLButtonElement and customElements.define() takes a third argument - {extends: 'button'}?
Do your custom elements work perfectly fine in Safari?
In your custom elements, how are you rendering the markup? Are you using innerHtml (vulnerable to injection attacks) for two-way binding, or a better option?
This is a unrelated question to your stack. I am using EmotionJS in my React component library, how can I migrate my EmotionJS styles to SASS styles?
Sass paved the way but it's time to ditch it.
Mixins... I FKN love them. They are excellent at naming certain CSS "hacks": things that only make sense together, things that must act together otherwise the effect you want won't happen. Now you could write comments around it, but named mixins equate well-named, descriptive function names from clean coding.
if they could be strongly typed I might agree with you, but all they end up doing is hiding complexity.
Sorry, I don't get it. Do you have an example?
Good old BEM days with Knockout JS, not many React devs would know about BEM styling
BEM is really good i learnt from jonas sass course and it was really fun utilizing block, element and modifier classes
"Nested CSS rules ... are a maintenance nightmare."
Use things they way they're intended. If css rules are only intended to work in, say, tables, then nest them inside of tables. Or, just certain classes of tables. If they are to be general, then don't nest them.
These three .hilite classes will not conflict with each other; the deeper ones will always override the shallow one at the bottom, but only inside .french or .italian tables. Hilite will be orange in french tables; green in italian tables, and blue everywhere else in your whole app. If you want .hilite to do something else outside of your zoot-suit panel, write it that way:
Any HTML outside of the .zoot-suit panel will have to make their own style rules; they can use .hilite freely without any orange, green or blue bleeding in.
Only issue I have with this is that then you don't know if
.hilite
is something global, is it a utility class, is it something specific to that table only when you encounter it in HTML/JSX. If using.table__cell--hilite
for example you know that it's a highlighted table cell and you can get all that data without having to look at the source code. And you know that it should be a table only thing here since it's kinda namespaced withtable__
Also, your example uses same class name for global selector and then selector that is more specific to 2-3 scenarios which now makes that selector super situation specific and it doesn't necessarily convey its meaning just by reading it. In my opinion it also raises CSS selector specificity unnecessarily.
But to each their own, I have my preferences and you have yours. Lot of this stuff is of course also situational and cannot really be applied to 100% of projects/situations.
Always good to see some contrary opinions - not everything that "shines" is made of gold :)
To me both HTML-in-JS and CSS-in-JS look ugly, because you can't separate presentation from logic, hence can't divide this work between different developers without permanent merge conflicts.
I honestly disagree both with the article and some of the comments here. I'd like to address the first of the article. (Respect for the article, it does have valid points and concerns and it's well put together).
I have a lot of experience with a lot of solutions in a lot of different scenarios. A new solution to a current one can always feel better, because it excells in the painpoints the older solution had, those are the problems we're trying to address with the change, but that doesn't necessarly mean that the old solution wasn't performing better on other areas. For me CSS-in-JS (with optional static extraction and/or 0 runtime) is the perfect way, because it addresses many different issues I had through the years the most.
There are solutions that can static extract with 0 runtime and they are able to handle dynamic props using CSS variables, so at the end it's 0 runtime and only class names. I see you mentioned this at the end also, but there are some misconceptions here. The mentioned "problems" are not necessarly unique to 0 runtime libraries, there are solution which aren't suffering from those pain points.
The Bad
The Ugly
Performance
No JS involved, I don't want to go into it more deeply than that.
Styles are still inserted when a component mounts for the first time... Not necessarily true. It's really based on the extraction strategy you choose. You can choose to extract everything in a single file. You will end up with the same "problem" using CSS Modules if you don't static extract into a single file btw.
Inline styles are known to cause suboptimal performance when applied to many elements... They can, but it's nothing you should worry about, especially that it's just CSS variables. The overhead of loading an external stylesheet and handling dynamic cases with code (conditions, className concat, etc) will actually perform worse by adding more overhead imo.
BEM
I'm incredibly against it. 1 HUGE benefit of CSS-in-JS (inline) is that you no longer need to think of naming and organizing your selectors/styles. Best conventions are the ones that are not needed ;) Naming things, especially using BEM is a huge overhead during development, it significantly interrupts your flow.
CSS Modules
style.container
actually is. Having them inline during development is a huge productivity boost for me.CSS-in-JS with runtime
I'm not against runtimes tbh. I do have a fairly large social site with millions of contents and several multimedia types and complex user features. The complete client side codebase is 25MB in total bundle size (no gzip), yet my initial JS for a single page is 200k (no gzip), which is mostly React, it uses Styled-Components with CCSS and uses a custom, single file SSR pipeline (no Next or whatsoever) with an isomorphic codebase. I have my Lighthouse scores at 96-100 points. Could be faster w/o runtime? Yes. Would my users notice anything from it? Not really. So why would I choke myself by using a less productive/comfortable solution? :)
Extract on your own
It's not a too hard thing to write a Babel plugin that extracts the static parts of your
css
prop however you want. By that you save a lot of time spent on refactoring to a new solution on a large codebase.This is the most thought-through piece of writing on this entire page. Everything you wrote about SCREAMS "this developer is experienced AND practical and makes great points". Something felt slightly off about OP's post (like saying "inline styles are suboptimal for performance" but...citation needed??)
You should post an op-ed to this with the contents of this comment. It's a worthy article in itself. You have actual use-case-level information on how it doesn't even affect performance in any way a user would ever notice. That's a very valuable contribution to this topic and the entire community would be better for it.
Either way, thanks for the very well-constructed comment. It was very helpful in adding balance to this conversation.
Can we get a link to that site? If that wouldn't work, a performance recording. It's peanuts to tell on a flamechart whether a site does or does not spend a significant amount of time on it. I'm not suggesting it doesn't have reasonable performance, I'm just interested to see how it can be implemented performantly.
What OP mentions is not uncommon and often is even worse than a 2X difference. Although it's hard to attribute because generally it means nobody really checks performance anyway on sites that use it, so there could be other issues. But the point is also a bit that it aggravates existing problems and can easily push you beyond the performance budget.
You seem to be concerned only about bundle size, but it's not the main issue. As the author indicated, it makes components much slower to render, because it needs to walk through your theme's complexity, over and over again. You're creating a runtime representation of your styles for React to apply to the DOM. And React is written to handle the fact that things can change. But the vast majority of your styles doesn't change after the page is loaded. Your pumping significantly more data into React and get nothing out of it you couldn't do with a static stylesheet.
Another face of this problem is that you can't update your theme without rerendering most components, which can get very expensive
Here's an example of the kind of mayhem it can cause on Reddit, where switching a theme takes over 100ms of scripting alone.
The performances issue was always a technological limitation but the DX of CSS-in-JS is the best to handle CSS in a codebase imo. Pre-compiled styles are solving this and it will keep on improving!
Now the reason this is better than Tailwind is that you do not need to learn anything new. As long as you know CSS you don't need a documentation. With Tailwind you're constantly juggling between the doc and your code (ok now we have auto-completion) and there is a learning curve. CSS-in-JS has a learning curve close to zero.
I have way too little time to be going over how to construct an entire system that fixes all of these things, which is why my vote goes to eliminating CSS-in-JS. Wish React would instead do Single File Components so that people can start learning to separate concerns.
Overall fantastic analysis on css-in-js, learned alot for sure and appreciate the time you took to analyze everything.
I am struggling to understand how you reached your conclusion for picking bootstrap though. while bootstrap does have utility classes its main use case is telling your app how it ought to be.
Tailwind intends to provide a utility class framework with basic defaults. Tailwind does NOT enforce any styling on your components like bootstrap does with things like its btn class
If you are morphing bootstrap into something to fit your design system i believe you would be much better off creating a design system in Tailwind instead.
if you have more info on why you picked bootstrap besides that you were familiar with it, i would love to understand further because everythinbg described including the ovverides you are doing to bootstrap scream at me that you would be better off with tailwind
We're not using all of Bootstrap, just their utility classes (which we have customized quite a bit). I'm not very familiar with Tailwind but I think they may have taken utility classes a bit too far.
Tailwind will now tree-shake its styles down on build, removing all unused styles.
Afaik they don't tree-shake anymore. The default for quite some time now is the JIT runtime, which only creates the CSS rules that are needed in the first place :)
You will absolutely love Tailwind, trust me bro
Bro, I personally used Tailwind and I can say it is really good solution but... it is really good solution for only a specific parts of styling, namely, for utility styles (mostly common & repetitive styles like positioning, padding, margins, flex, ...). But applying Tailwind for everything is a mess and vastly clutters codebase. Besides, it has steep learning curve (it may not be felt while you are working solo but in a team with noticeable numbers you can really see this). It has its own place in DX but it is definitely not a silver bullet.
Wether it clutters the codebase or not is up to you.
There is nothing preventing you from making your normal classes and use the @apply directive.
.myClass {
@apply rounded-full mx-auto text-primary-500 my-8
}
So in that regard there is no difference.
Yes. And try to manage hundreds or thousands of this kind of meta classes across a huge codebase. You are adding yet another unnecessary abstraction layer on top of already provided abstraction... Don't think in terms of just you coding by yourself and setting conventions on how to use it, maybe it works to some degree, but not in a team .
We've reached a lot of similar conclusions here at Wix.com too. Our first HTML editor (and following versions) used an in-house CSS-in-JS solution, but eventually the performance costs just got too high.
I'm the team leader working on Stylable, our solution to this problem space. Stylable is a CSS superset that adds a type-system, automates the BEM scoping part, and provides templating tools to improve developer experience and reduce code duplications.
We've been battle testing it internally at Wix for several years now (used in over 50+ million sites), and are now beginning to reach out to new users and to build our community. We'd love to talk if this sounds interesting to you.
Hey @tomrav, Stylable looks cool. It would be interesting to hear what benefits it provides over something like Sass Modules. E.g. what problems does Stylable solve that are harder to address with existing tools?
Glad you liked what you saw, I'll elaborate a bit on our approach.
Sass and CSS modules are both well proven and battle tested solutions that we absolutely love, but have their downsides too. I would say the most notable differences between Sass modules and Stylable are the following:
Sass is oriented for CSS and document based projects in its core. CSS modules tries to bend Sass' approach to be more component oriented. Stylable was built with components being treated as first class citizens, allowing stylesheets or their parts to be utilized by our module system in various ways.
The namespacing that CSS modules provides is there to prevent collisions, and not improve readability or debugging experience (unlike BEM, which in my opinion does both). While Stylable combines both approaches, opting to use a configurable automatic BEM-like namespacing for its output.
At Wix a single component can have many drastically different use cases - to provide flexibility for such components, Stylable encourages treating your component styling as just another part of its API. Each stylesheet exposes its inner parts allowing to customize and theme any part of the application easily from the outside.
Stylable adds a type-system to CSS (currently, only at the selector level, with more of the language to come). This allows defining inheritance and relationships across stylesheets, allowing our custom language service a much better understanding of the code, resulting in advanced diagnostics, auto-completions, code navigation and other language features.
Lastly Stylable namespaces a lot more than just class names, also namespacing keyframes, custom properties, layers and (very soon) container queries to keep you safe from leaks or collisions.
I'll be happy to answer any questions here, or chat over zoom sometime if you'd like to hear more.
@tomrav That sounds pretty awesome! I agree the BEM-style class names would help a lot with "debuggability" over the class names you get from CSS Modules.
It sounds like you have some pretty complex components at Wix. Or rather, you have components that need to be fully themable and work in many different contexts. We don't really have this problem at Spot so I am not sure if Stylable would be as necessary for us.
The additional namespacing features sound nice too. That is one thing that could make Sass hard to scale (though I'm not necessarily up to date on the
@use
directive in Sass, I still use@import
.)Feel free to email me if you'd like to talk more. srmagura(AT)gmail.com :)
In Wix we definitely have some complex cases, but (as biased as I am), I feel that Stylable also offers a lot of value to simpler projects by providing an improved developer experience with type safety.
Why people keep hyping tailwind is beyond me. Nothing about compiling a list of utility classes before you run your code, sounds at all useful to me... It literally prevents you from doing the easiest dynamic stuff because of the limitations of your utility classes and the only way around that is to do custom DOM manipulation to change css variables at runtime. Why do people still think this is a good idea? Why not use the benefits of css in js and have it compile to plain css files when building? Seems like a much more sensible and maintainable solution to me. Also, having all those utility classes that you don't use, creates far more overhead... CSS files have to be downloaded by the browser too... You literally have code in production that never gets used... I thought we moved past that :(
Sorry, but your argument about having unused utility classes is incorrect. Tailwindcss for a long time now compiles only the classes that are actually being used in your code.
Would be great to fact-check your arguments before you post since it might mislead those not familiar with the topic.
Ok, what about the other argument?
I've been using tailwind with Vue mainly, for dynamic stuff you would just conditionally apply different utility classes, no need to use CSS vars. Can't complain about the DX.
The only negative side I can see here is that in order to reuse your components in another project that project would need Tailwindcss set up as well, so they're not as portable. But that might also be true for css-in-js solutions, right?
In this sense web components would be the best probably, I might be wrong though.
You cant conditionally apply dynamic values. You also cannot access defined constants in tailwind with JavaScript, so if you need to apply custom logic based on theme breakpoints you have to define and manage them in two places.
By constants, do you mean tokens? because I experimented with tailwind in a project recently, it was ok. but i definetly appreciate the css-in-js route more these days.
Anyway I generated tokens from my config with this prelaunch tool:
and because i was using yarn 4, in the
package.json
:and then i just made a simple provider
I also thought so, but you can actually avoid custom DOM manipulation through changing a single theme class via your declarative rendering library (React):
youtube.com/watch?v=TavBrPEqkbY
When you do that, the top level component with that theme class will re-render with a new theme class (but none of it's children will need to re-render since their props didn't change so the React VDOM diff will exclude them). Then, due to the theme class being a CSS variable, all the local styles scattered about the app, that reference the CSS variable, will update with the new theme during the CSS cascade. That way, you avoid the rendering de-optimization that you'd otherwise have if you changed all the local styles through re-rendering the entire app with JS.
Hey lead on the Compiled CSS-in-JS project here 👋. Just thought I'd clear up some aspects of the Compiled library and our future roadmap.
For the components that are included at runtime, those are intended for use in development only. For production use, we're going to recommend people use stylesheet extraction which moves all the CSS into an atomic stylesheet. That'll mean that all the runtime react components will be stripped out. We haven't fully made this clear in documentation yet as we're still ironing out some issues with dev experience.
The dynamic prop support is intended as a migration feature to allow people to adopt compiled without fully re-writing their code. We required this as part of our migration journey at Atlassian. It does end up with non-optimal performance this way so we're we'll be building more tooling around writing fully static CSS.
At Atlassian, we did a deep dive into whether to use CSS modules or continue with CSS-in-JS. We did end up selecting Compiled as our best path forward for a few reasons, but to simplify:
Charka-UI sweating right now reading this article😂
I don't think so. Full disclosure I was a contributor on Chakra-UI.
Sage has been aware of the issues with Chakra and it's in their doc. He's been working on ZagJS which uses state machines to provide unstyled components. I assume that one could use CSS, Tailwind or CSS-in-JS to style those components. This is a YouTube video of Sage talking to Lee Robinson about The Future of Chakra/ZagJS.
While I agree with the author about CSS-in-JS, being "the 2nd most active maintainer of Emotion", he's been a contributor to the problem and previously thought of as a contributor to THE solution. We've all been there. If you haven't yet, just wait.
Shouldn't React with their vdom be sweating given projects like Svelte, Solid and Qwik? If you laugh at that remember that once JQuery and Angular JS ruled the world. They were each great for their time, like Emotion, but eventually every king falls.
What exactly is Svelte and Solid except the idea that surgical dom mutations are better than the complexity of a reconciliation engine. Now that we have modern optimized browsers, the svelte/solid model makes sense. A really big app with lots of dashboards and data feeds...do you really want that to be in svelte? Hrm.
There's also module federation where you sortah want css in js or you'd have to manually include a copy of extracted css into every federated module.
And you have to worry about tree shaking when its a .css file.
not that that's a big deal but it's an additional worry.
You could make the similar argument of using emotion to do dynamic css, and then using tailwind utility classes for the majority of your use case. Then you could have your cake and eat it too.
Hahahahah
You should've just went with Tailwind tbh
Bootstrap was just convenient because you were already familiar with the classes but there's so much you could've gained by using Tailwind, like JIT compilation (on demand one-time utility classes, variants for element state and media queries, ability to change things using a configuration file, etc)
And Tailwind is not opiniated, unlike Bootstrap.
110pct agree with all your points
Have you ever looked into Vue's style system? I'm not sure how they do it, but you simply add a "scoped" attribute to a style tag and it automatically scopes the CSS to the components. Their CSS is also compiled at build time into its own file without the need to render separate components. This is the one thing that kept me from switching to React for years. Their styling just works and it works really well.
I haven't messed with Vue 3's implementation, but they also allow you to use JS variables, which are rendered as inline styles: vuejs.org/api/sfc-css-features.htm...
Vue adds a hashed attribute to the dom element, for example:
data-v-ikd87k9
.Then your CSS is transformed using PostCSS to target CSS-selectors with your attribute, for example:
.my-class[data-v-ikd87k9] {}
I consider it a train-wreck when I can't view my DOM and see all the class names. I don't like my class names being mangled like that hash thing you mentioned. I'm fine with SCSS and not having styles scoped to components, because lots of my styles are cross-component anyway.
I agree, personally, I consider a readable DOM to be important, which is why I try to avoid Tailwind :)
However, I've never experienced Vue's hashing to have impacted this negatively.
What about Tailwind is not readable? If anything it's more readable.
And nothing prevents you from making custom classes composed of Tailwind helper classes.
Tailwind is very difficult to read when you get into more complex cases, especially when working on teams where not everyone knows Tailwind utilities. Having a class that actually describes what the purpose of the element is in addition to styling it is far more readable, takes up far less space, and can still allow for the use of utility classes where they are appropriate for example adding layout and margins.
Vue does not mess with the class names you add to elements, it just appends a data-hash attribute.
Angular does this by default.
Very very nice
Thank you for sharing your insights. I agree (judging from my current view point of past and present projects and experiences) that CSS-modules paired with SASS/SCSS are the cleanest and sanest approach to component styles.
But I would say it also checks the third point of your three-point list of good things (locally-scoped styles, co-location, JavaScript variables in styles):
You can share values between styles and js code, in both directions, during build time and during runtime. Each scenario requires its own mechanism (I want to cover this in a blog post but don't know when I will find the time), here is the gist of it:
:export
getComputedStyle()
sass-loader additionalData option
element.style.setProperty('--some-prop', value)
In my webpack config I share the colors (in a *.ts file) with my styles by generating variables like
$primary-main
: