Styled Components deep dive

Published on: Tue Oct 26 2021

Series

Content

Introduction

css-in-js has been the new “hot” thing for some time. They solve a lot of problems related to managing styles on large projects.

At a high level, these libraries helps you to colocate the styles at the component level which helps with maintainability. So, instead of reasoning about styling globally, you can reason about it at the component level.

I think libraries like emotion and styled-components really hit the sweet spot for a integrated developer experience with React.js.

With the recent major release of material ui, the material-ui team have also moved to adopt emotion internally, and to support styled-components styling engine.

One of the core team members, mnajdova, has done a very comprehensive analysis into the css-in-js solutions to determine which is the best fit to migrate to and support natively. The full analysis can be found here.

I personally think you can’t go wrong with either one. However, styled-components is still a more popular solution.

So, today we are going to dive into the internals of styled-components and take a look at what is happening behind the scenes when we create and render styled-components .

Features

Here are some of the features it supports:

  • automatic critical css (SSR)
  • dynamic (run-time) css via props
  • classname hashing
  • “caching” of generated styling
  • automatic vendor prefixing
  • Theming through ThemeProvider
    • theme is automatically added to the the tag template literal when styling
    const StyledParagraph = styled.p`
      color: ${props => props.theme.palette.main};
      font-size: 1rem;
    `;
    
  • supports react-native

Client

Tradtionally, css is built and shipped as one file to the end user. css-in-js works in a similar way but slightly different. Let’s dig in deeper.

styled-components and other css-in-js libraries go hand-in-hand with React.js’s concept of “thinking in components”. This is where we break up a design into components, and build them individually. Then, when we are done, we combine them into a holistic page or feature like lego blocks.

thinking in components

The css sits with the component, and is scoped to be used only at that level. If you have used styled-components, this is achieved through their hashed classnames.

Example folder structure:

components/
├── Button
│   ├── index.ts
│   ├── styles.tsx
│   ├── Button.tsx
│   ├── types.ts
│   └── utils.ts

Each of these classname are unique in the global scope so no two component can be sharing styles unless they are specified as “global”.

This becomes really handy when making changes because these changes will be isolated to the individual components rather than being applied globally in the your project.

Overview

Internally, styled-components builds a wrapper for all the available DOM elements (div , a , p ...) on styled so then in your code you can do the following:

import styled from 'styled-components';

const StyledParagraph = styled.p`
  color: gray;
  font-size: 1rem;
`;

const StyledDiv = styled.div`
  padding: 2em auto;
  margin: auto;
`;

Then in your html, this will be generated when the component renders:

<html>
  <head>
    <style data-styled-version="5.3.1">
    .ixRGje{color: gray;font-size:1rem}/*!sc*/
    .TTyoQ{padding: 2em auto;margin: auto;}/*!sc*/
    </style>
  </head>
</html>

How does it go from the component being rendered to the style being inserted into the html ? Let’s look at that process in detail.

Style injection

client side styled-components flow
  1. The Component first renders
  2. The tagged template styles are processed and custom props are provided via react hooks
    • The theme is provided via the ThemeProvider where it uses React.Context to provide access to it
  3. As styles are being processed, an unique hash (classname), componentId and “group” are generated
    • This is mainly to keep track of what styles rules have been added
  4. The styles are inserted into a stylesheet instance and tracked internally
    • the stylesheet instance is provided via the React.Context (This is used both on the client and server)
  5. The styles are further processed by stylis which adds pre-processing for the css rule
    • All these rules are kept in the Sheet.ts
  6. The styles get inserted into the html (DOM)
componentId is important because internally styled-components determines whether or not this style has already been inserted (at step 6) before inserting to avoid duplicates

There are a few details I left out where styled-components actually keep track of these styles as groups inside a Uint32Array where base size is 512 . As the style and style variants are added, the indices are incremented in the Uint32Array .

Likely to have a memory efficient way to keep track of how many styles are available.

The GroupIdAllocator handles all of these mapping from group id to unique hash classnames and vice versa. Judging from the code, this is used for re-generating css from the stylesheet rules stored.

Reference:

Caching

As I mentioned, there is some “caching” happening when we render the styled-components . This is based on the generated componentId , so whenever we render the component, the library would insert the rule and keep track of the id.

Internally, when it calls generateAndInjectStyles, it would ensure that the componentId does not already exist in its mapping via stylesheet.hasNameForId before making an insertion.

This prevent unnecessary insertion of duplicated css into the html or into the output of the critical css which we will cover next.

Server

Critical css is important for first contentful paint (FCP). If you just serve the html and css required from the start then the users should be able to see the page right away (as soon as html is served and parsed) and be able to interact with it as soon as javascript loads in.

Whereas if you serve a large bundle (html, css) for the first time, this would block the rendering of the html until resources are loaded. So, users are left staring at a blank page for longer than usual. The bounce rate on the sites will be high in this case.

When you start to consider edge location, for example, serving customers in the Asia region when you are operating in North America then it may make a difference.

client side styled-components flow
Comparison of an application load times that has a large css bundle with critical css and without critical css (Chrome - Emulated, 3G fast, running on Vercel)

This is not something exclusive to styled-components but the library just has this feature built in, and makes it very easy to do with some minor setup in your code.

On the sever side, styled-components operates slightly differently as it does not have access to the DOM. So, the style rules are stored internally in an array. Refer to makeTag() and VirtualTag.

Server side rendering

The server side generation of css works very similar to the steps described above for client side css generation.

The only difference is we are providing a new Stylesheet instance via React.Context to the React children when we call sheet.collectStyles() and rehydration.

The styles are stored internally on the object, then it gets generated and “sealed” (can’t be re-used again, it throws an error) when we call sheet.getStyleTags() which would provide us all the necessary styles to style only the initial mark up of our page.

Here is what the flow would look for the server and the client rehydration:

client side styled-components flow

Basic implementation of Server side rendering:

import { renderToString } from 'react-dom/server'
import { ServerStyleSheet } from 'styled-components'

const sheet = new ServerStyleSheet()
try {
  const html = renderToString(sheet.collectStyles(<YourApp />))
  const styleTags = sheet.getStyleTags() // or sheet.getStyleElement();
} catch (error) {
  // handle error
  console.error(error)
} finally {
  sheet.seal()
}

Rehydration

The styles provided as the critical css need to sync its state with the client side library that is loaded in. styled-components has automatic rehydration built in.

The critical css injected has an unique identifier in the style tag. That way, the client library can identify it and parse it from the html to update its internal state to reflect the styles already available.

Then as the user interacts with the page, more css rules are added to the <style> tag as needed on the client side.

Example of the critical css in html:

<html>
  <head>
    <style data-styled-version="5.3.1">
    .ixRGje{-webkit-text-decoration:none;text-decoration:none;font-size:1.25rem}/*!sc*/
    .TTyoQ{-webkit-text-decoration:none;text-decoration:none;font-size:1.25rem;font-weight:500}/*!sc*/
    </style>
  </head>
</html>

References:

Room for improvements

I think there can be some room for improvements as generating css at run-time has some cost associated with it especially on low-end devices.

If you are running critcal services (ie healthcare services, non-profit) in third world countries it may make a difference. The individuals using the services would likely not have access to powerful devices like the latest iPhone or Android devices.

So, extra consideration should be made to optimize for accessibility.

When we are generating all those styles at run-time, the experience will likely be very different on thoes devices.

One approach I really like is the adaptive loading approach presented by Addy Osmani from the Google chrome team. The gist of it is to ship the minimal core interactive features first, then determine whether or not to enhance the website or application based on the user’s device requirements (ie data plan, hardware).

Of course, it may not be an issue but its better to benchmark and determine what your customers are using then optimize based on that.

Conclusions

I hope this deep dive into styled-components has been helpful. Having used styled-components for some time, I found it quite interesting to understand how this all works behind the scenes.

The library does a lot of heavy lifting for you behind the scenes but I think understanding the mechanism internally allows you to better understand it’s drawbacks or strengths. Sometimes even make changes to better suit your team’s and project’s needs.

I think css-in-js really hits the sweet spot for developers working with css and React.js for most projects. It really improves the confidence when making changes to css on large project and comes with great built in features like critical css. Also, as I briefly touched on, it fits React.js’s component model really well.

Lastly, when using css-in-js solutions like styled-components , extra considerations should be made when trying to make websites more accessibile to users on low end devices.


Enjoy the content ?

Then consider signing up to get notified when new content arrives!

Jerry Chang 2022. All rights reserved.