Skip to content

CSS in JS type checking

How the introduction of the CSS in JS can improve the first render time of a Progressive Web Application

CSS in JS is a relatively new technique that lets developers write CSS code directly inside the Javascript code instead of having separate CSS files.

The key benefits are:

  1. Dead code/rules and tree shaking: By using Javascript tools you can easily detect unused CSS rules and remove them from the bundle.
  2. CSS isolation: CSS class names are dynamically generated; they are typically smaller and more isolated than regular CSS.
  3. Download size: Small CSS names and the embedding of CSS in the application bundle leads to fewer network requests.
  4. Javascript flexibility: Even though several tools (SASS, Less, etc) exist to write CSS more efficiently, nothing can beat the power of a full programming language!
  5. Code sharing: All code is in the same location and therefore you can easily share constants like colours and sizes between CSS and application code.
  6. Static analysis: Use of standard Javascript tools makes it possible to ensure no invalid CSS rules or values are used.

The usage of CSS in JS is obviously not the solution to all performances problems and there are many developers who heavily criticize the concept behind it, for example, posts such as this . Several solutions also exist that enable important performances to benefit by just using plain CSS code, for example, CSS modules . As always, as developers, we just have to choose the tool and technique that best fits the product requirements and team preferences.

In this article, we demonstrate how to embrace CSS in JS in a React application.

Choosing a CSS in JS tool

There are several open source CSS in JS tools available. Looking at a non-exhaustive sample list like this there are more than twenty options.

All the tools provide more or less the same functionality and most of the time are framework-agnostic, so the choice is really up to you.

In our case, we want to choose tools that are easy to introduce in the stack without too much complexity. So all tools that use the Higher Order Component (HOC) approach are excluded.

HOC uses the idea that a developer passes a component to a function which adds new functionality. The result is then used in place of the original component.

Similarly, one of the benefits we wanted was static type analysis, therefore tools that used ES 6 template literals to write the CSS are excluded as well. Only tools that accepted CSS as Javascript objects are considered for inclusion.

Amongst all existing CSS in JS tools, we chose TypeStyle for the following reasons:

  1. It uses plain Javascript to write code instead of enforcing usage of Javascript templates and similar.
  2. It has complete TypeScript typings. For editors that support it, like Visual Studio Code, it enables complete static analysis and unused code detection at development time.
  3. It is framework-agnostic and can be embedded easily into most frameworks without fundamentally changing the way you write your application.
  4. It has utilities and related packages for media queries, colours and animation.
  5. Since rules are appended to a <style> in the page, generation of the bare minimum CSS in server-side rendering (the so-called critical path) is automatic.

To see it in action check our React PWA application. nearForm wrote about it in this blog post recently. You can see the application in action at https://hackernews.nearform.com and check out the source code on Github.

How to write CSS in JS using TypeStyle

The core idea of writing CSS in JS is that you call the style function passing your CSS rules. The return value of the function is a class name you can use when you need.

The generated class name is something like f14svl5e. This name (actually is a hash of passed properties) is generated from the rules you pass in. There is no collision. Also, calling this function automatically appends this rule to the list of rules to register.

To get started, let's define our rules. We create a little spinner using TypeStyle in React:

Plain Text
const React = require('react')
const { style, media, keyframes } = require('typestyle')
const { px, rem, percent } = require('csx')</p><p>const green = rgb(0, 255, 0).darken(0.2).toString()</p><p>const animation = keyframes({
  [percent(0)]: {
    transform: 'rotate(0deg)'
  },
  [percent(100)]: {
    transform: 'rotate(720deg)'
  }
});</p><p>const spinnerContainerClassName = style({
  width: rem(10),
  height: rem(10)
})</p><p>const spinnerClassName = style(
  {
    stroke: green,
    strokeWidth: 3,
    strokeDasharray: percent(300),
    strokeLinecap: 'round',
    strokeDashoffset: 100,
    fill: 'transparent',
    animation: `${animation} 2s linear infinite`,
    transformOrigin: 'center'
  },
  media({maxWidth: px(300)}, {
    animationDuration: '0.5s'  
  })
)</p><p>function Spinner() {
  return (
    <svg className={spinnerContainerClassName} viewBox="0 0 60 60">
      <circle className={spinnerClassName} cx="30" cy="30" r="15" />
    </svg>
  )
}</p><p>class TodoApp extends React.Component {
  render() {
    return (
      <Spinner/>
    )
  }
}</p><p>ReactDOM.render(<TodoApp />, document.body)

In this example, we demonstrate most of the features you typically use.

Firstly we define a colour using,csx which is a utility package from the same developers of TypeStyle.

Then we define the rules for animating the spinner. In this case, we take advantage of the newer Javascript syntax and the concept of computed keys. The values are just CSS rules, the only change needed is to rename all attributes in camelCase.

Once again, the keyframes function returns a string - we don't care what it is, we just have to pass it over to the style.

Then we define the two classes needed for this example: one for the SVG itself and one for the inner circle that spins. In the latter we reuse the animation name; this is unique across all generated styles so no collision happens.

We have also defined a media query using the media function. This takes the media query as the first argument and the rules object as the second argument. To test it just make the window smaller and the spinning velocity increases.

Lastly, we pass the returned values as class names as the values of the className property and were done!

Once the application is loaded and the style functions are evaluated, TypeStyle creates a <style> tag in the document head and appends all the rules there.

This is the code that is generated:

Plain Text
<body>
   <svg class="f1f6g1bb" viewBox="0 0 60 60">
       <circle class="f1wqo8mf" cx="30" cy="30" r="15"></circle>
   </svg>
</body>

And this is the added <style> element:

Plain Text
<style>
@keyframes fyn2kv5 {
  0% {
    transform:rotate(0deg);
  }
  100% {
    transform:rotate(720deg);
  }
}
.f1f6g1bb {
  height:10rem;
  width:10rem;
}
.f1wqo8mf {
  animation:fyn2kv5 2s linear infinite;
  fill:transparent;
  stroke:rgb(0,153,0);
  stroke-dasharray:300%;
  stroke-dashoffset:100;
  stroke-linecap:round;
  stroke-width:3;
  transform-origin:center;
}
@media (max-width: 300px) {
  .f1wqo8mf {
    animation-duration:0.5s;
  }
}
</style>

Generic Rules and Advanced Media Queries

TypeStyle also lets you define rules with custom selectors. These rules are applied immediately to the document and no class names are generated. Let's say we want to show our spinner in the centre of the page. We take advantage of the CSS Flexbox module and this task is trivial.

Plain Text
typestyle.cssRule('html, body', {
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  minHeight: percent(100),
  fontSize: '10pt' // This sets 1rem = 10px
})

Going back to media queries, there is something to consider when working with them. While CSS is applied top-bottom, TypeStyle rules are not. Collision-free classes names usually ensure this is not a problem, but media queries targeting the same class may show some unexpected behaviour.

Take this stylesheet example:

Plain Text
div {
  color: black;
}</p><p>@media (max-width: 600px) {
  div {
    color: red;
  }
}</p><p>@media (max-width: 400px) {
  div {
    color: yellow;
  }
}

When applying this to a document the media queries are executed top-bottom, therefore when the window size is below 400 pixels the div text colour is yellow.

When using TypeStyle, you typically translate the stylesheet above to the following definition:

Plain Text
const divClassName = style(
  {
    color: 'black'
  },
  media({maxWidth: 600}, {
    color: 'red'
  }),
  media({maxWidth: 400}, {
    color: 'yellow'
  })
)

TypeStyle doesn't guarantee the order of generation of the classes (especially since this is dependent on the JS engine), so we need to adapt the media queries not to generate ambiguity.

The code above can be fixed this way:

Plain Text
const divClassName = style(
  {
    color: 'black'
  },
  media({maxWidth: 600, minWidth: 401}, {
    color: 'red'
  }),
  media({maxWidth: 400}, {
    color: 'yellow'
  })
)

Server-Side Rendering and Critical CSS

TypeStyle makes Server-Side Rendering (SSR) very easy. The core idea is that once you get your server-generated HTML ready in some variable, you make a call to the getStyle function to flush all the used styles so far.

This also has the benefit of ensuring that only used CSS rules are sent in the response - this is the so-called critical CSS .

Here is a simple example of how to generate a full HTML page using React and TypeStyle on the server:

Plain Text
app.get('/', function (req, res) {
  const body = ReactDOMServer.renderToStaticMarkup()</p><p>  const html = ReactDOMServer.renderToStaticMarkup(
    <style>{getStyles()}</style>
  )</p><p>  res.send(html);
});

Drawbacks and Improvements on the age Performances

Embedding TypeStyle in the code, of course, has its drawbacks. In particular, the addition of the 2 new libraries increased the PWA application code size by roughly 15KB.

The good news is that while this additional data is loading, the application and the user interaction is not blocked at all. The application has already been rendered on the server and all the CSS required has already landed on the device with the HTML page; so the browser has everything it needs to render the page.

Also, regarding the increase - when in fast network environments such additional data is hardly perceivable, on slow or unreliable networks it is balanced by huge benefits on the first-render time.

This was the original performance of page load on the original branch using just plain CSS.

By using TypeStyle, we've been able to almost reduce the first render time on a slow 3G network by 50%.

To give an overview on how TypeStyle helps in limited situations we have run the same comparison on a different mobile network condition using the profile settings used by webpagetest :

Name Download Speed (Kbps) Upload Speed (Kbps) Latency (ms)
EDGE 240 240 840
2G 280 256 800
Slow 3G 400 768 400
Fast 3G 1600 768 170
4G 9000 9000 150
LTE 12000 12000 70

As you can see from the graphs below the benefits are more perceivable in the slower networks where each network request is really expensive. Even in most performant network; embedding using the CSS in JS technique reduces the first render time by 100ms.

In all cases, CSS in JS increases the first interactive at most by 100ms. As outlined before, the decrease of first render time makes it acceptable if we look at the increase in users conversion rates.

Conclusion

The introduction of PWAs has radically changed the way we develop our applications. Mobile devices are now capable of storing information offline in ways that were not even imaginable a few years ago.

PWAs introduce various new ways of serving content in unforgiving network environments. We must not forget that there needs to be a network to load the cache in the first place. Every application is different and the introduction of service workers allow developers to provide custom caching strategies which can better fit the specific use case.

In this post we have outlined how using CSS in JS can help in reducing the application size and the amount of network requests required to first load it. While this technique is not a silver bullet for all modern Web application performance problems, it leads to a significant reduction in the first render time.

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact