Zero runtime CSS-in-JS : Is this where great DX meets top-notch Web Performance?
This was a novel task for me. As a frontend speed & scalability guy, I often get involved with the websites when their frontend choices & setup are long in place. And often, my first job is to find the solutions living within the constraints of those choices. But, here, I had to provide inputs to help make that choice. 😊
Background : CSS-in-JS, DX and Web Performance
Before I talk about the findings from the exercise, here’s a quick brief on the friction in the web development world with respect to CSS-in-JS, good developer experience (DX) and web performance.
Modern UI frameworks have components as the basic unit of the frontend code. This allows us to build large and maintanable web applications. But, regular CSS doesn’t directly fit into this component centric model. Writing maintainable CSS that can fit well here requires us to follow methodologies like OOCSS (Object oriented CSS) and a lot of consistent discipline.
CSS-in-JS libraries like styled-components and emotion solve this by enabling us to write component-centric styling. Having the styling code closely tied to the component JavaScript enables dynamic styling and easier maintenance. This, in-turn, results in an improved DX.
But, making the UI styling JavaScript driven also means we make it JavaScript dependent at runtime. This has performance implications:
- All the additional styling related JavaScript adds to the parse, compile and execution time on the visitors' browsers.
- The UI rendering gets delayed as the styling JavaScript often executes after a lot of other JavaScript (because, SPAs!).
- The styling may not render if the JavaScript fails to execute. JavaScript errors are a lot more probable than CSS or HTML errors.
As a result of this, all the DX goodness of CSS-in-JS and top-notch web performance appear like an either-or.
Speed Impact of styled-components
So, what is the speed impact of using CSS-in-JS? To measure this, I created two versions of the below page (with NextJS) - one version styled via CSS modules and another with styled-components.
And, below are the measured performance numbers:
Based on the above:
- When using styled-components with the right optimizations, the start of page render during loading of the SPA can be at-par with CSS modules based approach.
- But, the overhead of the additional JavaScript exhibits some overhead in interactivity and re-rendering of the components.
Enter Zero Runtime CSS-in-JS
I then decided to evaluate zero runtime CSS-in-JS. More specifically, I evaluated linaria and studied astroturf.
These are CSS-in-JS libraries so we get to write our styling within the component files. They extract the CSS into separate CSS files during the build process. They do not require runtime JavaScript execution for styling. These libraries achieve dynamic props based styling via CSS variables at runtime.
So, I created a linaria styled version of the same page used for benchmark. This gave an opportunity to compare how similar it’s styling is to styled-components:
const Button = styled.button`
border-radius: 3px;
margin: 0.5em 1em;
padding: 0.25em 1em;
${props => props.primary
&& props.color && css`
background:
${props.color};
border: 2px solid
${props.color};
color: white;
`}
${props => !props.primary
&& props.color && css`
background: transparent;
border: 2px solid
${props.color};
color: ${props.color};
`}
`;
const Button = styled.button`
border-radius: 3px;
margin: 0.5em 1em;
padding: 0.25em 1em;
background: ${props =>
(props.primary ?
props.color :
'transparent')};
color: ${props =>
(props.primary ?
'white' :
props.color)};
border-width: 2px;
border-style: solid;
border-color: ${props =>
(props.color)};
`;
const Button = styled.button`
border-radius: 3px;
margin: 0.5em 1em;
padding: 0.25em 1em;
${props => props.primary
&& props.color && css`
background:
${props.color};
border: 2px solid
${props.color};
color: white;
`}
${props => !props.primary
&& props.color && css`
background: transparent;
border: 2px solid
${props.color};
color: ${props.color};
`}
`;
const Button = styled.button`
border-radius: 3px;
margin: 0.5em 1em;
padding: 0.25em 1em;
background: ${props =>
(props.primary ?
props.color :
'transparent')};
color: ${props =>
(props.primary ?
'white' :
props.color)};
border-width: 2px;
border-style: solid;
border-color: ${props =>
(props.color)};
`;
Unrelated to performance, but something of paramount importance - linaria’s similarity can make it’s adoption easier and future migrations simpler. However, I’m not sure if other zero runtime CSS-in-JS libraries are as similar to styled-components as linaria.
Speed Comparison : CSS modules vs styled-components vs linaria
So, how does the linaria styled version of the same page perform compare to the other versions? Below are the performance numbers:
Based on the above charts:
- Styling with linaria reduces the amount of JavaScript shipped and executed when rendering the routes. This improves the interactivity and route rendering speed.
- The speed difference may be higher for larger screens and complex UI interactions.
Conclusion
Based on the undertaken benchmarks and having written (very limited) linaria styling, my findings from this exercise were as following:
- Zero runtime CSS-in-JS library like linaria definitely seems offer a better balance of great DX and top notch web performance.
- Linaria’s styling similarity to styled-components makes it’s adoption easier and future migrations simpler.
- That stated, the traction and the dev ecosystem for styled-components is miles ahead of linaria, astroturf or any other CSS-in-JS library (as of 2021).
Based on the above findings, we decided to undertake a more detailed evaluation of linaria. To do so, we plan to style a few components and pages for the new website with linaria. During this process, we plan to check on the following aspects:
- Whether critical CSS extraction for our website’s server-side rendering works smooth with linaria.
- Will some linaria limitations (like not supporting dynamic props based media queries) inhibit the styling work.
- Ensure all the stakeholders are aware of no IE support (since the CSS variables leveraged by linaria for dynamic styling do not work with IE).
Note : Source code for the benchmark used to derive the results shared above is located here.