Avoid to much CSS-in-JS

Published on

The other day I was working on a large react component containing a complex form. There was a fair amount of custom styling for a div inside of this form, and it used emotion styled components library to do that.

Stuff like this:

const RulesHeader = styled('h3')({ marginTop: '40px' });
const RulesListDiv = styled('div')({ margin: '15px 0 0 15px' });
const NoMarginH4 = styled('h4')({ margin: 'unset' });
const NoMarginTopH4 = styled('h4')({ marginTop: 'unset' });
const NoMarginP = styled('p')({ margin: 'unset' });
const ImgBullet = styled('img')({ marginRight: '5px' });

I went ahead and put this into CSS/LESS file with the same name as the component. Granted, by moving the the CSS out into a separate file some benefit of proximity is lost, but putting it in a file with the same name at least helps with discoverability.

As an experiment I flipped back and did this code in the dev branch after I had the feature branch how I wanted it.

window.counter = 0;

const RulesHeader = styled('h3')(() => {window.counter++; return { marginTop: '40px'};});
const RulesListDiv = styled('div')(() => {window.counter++; return { margin: '15px 0 0 15px' };});
const NoMarginH4 = styled('h4')(() => {window.counter++; return { margin: 'unset' };});
const NoMarginTopH4 = styled('h4')(() => {window.counter++; return { marginTop: 'unset' };});
const NoMarginP = styled('p')(() => {window.counter++; return { margin: 'unset' };});
const ImgBullet = styled('img')(() => {window.counter++; return { marginRight: '5px' };});

In the course of filling out about half of this form in a happy path sort of way the counter went well over 2000! I know none of these functions are actually heavy at all, and because of virtual DOM diffing most of those calls don’t actually end up doing any DOM manipulation, which tends to be the slowest part of web app code like this. Still, it’s a good illustration how each render cycle does trigger each of these, sometimes multiple times per render! Whatever the cost is, it’s not nothing! Whereas with plain CSS if there is no DOM repaint or reflow there is nothing for the browser to do. I think this is worth understanding as a cost of many current CSS in JS library.

This change times a 100 components maybe would have some improvement on app initialization speed due to differences between JS and CSS but mostly it feels like a different concern than the load/parse time cost of CSS bytes vs JS bytes.

Just looking at the DOM that is produced it was also significantly simpler with the plain ol CSS approach. Way fewer <div class="w35-494-30jr"> type things all over the place. Which again, those extra DOM nodes aren’t much maybe, but they aren’t nothing. Frankly, I didn’t bother trying to get good metrics on what kind of performance difference it made. However, I can say it made the Cypress tests just a bit easier to compose the DOM a little less verbose, and the component file a little less busy, and that is in addition to it running less javascript code.

Not all CSS in JS solutions are runtime based like this one is. The way Svelte does CSS inside of it’s component files and handles them at compile time feels soooo so much better to me, so I don’t think it is necessarily a problem of CSS in JS files, but CSS in the JS runtime feels mostly not good to me these days.