Increasing the performance of elm-css

Robin Heggelund Hansen
Bekk
Published in
7 min readDec 14, 2021

--

Most Elm projects I work on in a professional setting make use of a wonderful library, created by Richard Feldman, called elm-css. This library gives you statically typed CSS which can live alongside you view functions, making it easy to see the connections between HTML elements and CSS styling.

Recently, I came across a performance problem in one of our applications, triggered by the amount of work elm-css had to perform every time there was a change in state. While I did manage to fix the problem using aHtml.Lazy function, I walked away with a hope that elm-css could be made faster so that there wasn't a problem to begin with.

In this article, we’ll take a look at how elm-css works, and how I found a way to nearly double its performance.

The workings of elm-css

Let’s start off with a simple example. Below you’ll see the source code of a very simple elm-css based application:

module Main exposing (main)

import Css
import Html exposing (Html)
import Html.Styled
import Html.Styled.Attributes exposing (css)


main : Html msg
main =
Html.Styled.toUnstyled view


view : Html.Styled.Html msg
view =
Html.Styled.div
[ css
[ Css.backgroundColor <| Css.rgb 0 0 0 ]
]
[ Html.Styled.div
[ css
[ Css.color <| Css.rgb 125 125 125
, Css.textDecoration Css.underline
]
]
[ Html.Styled.text "grey text" ]
, Html.Styled.div
[ css
[ Css.color <| Css.rgb 125 125 125
, Css.textDecoration Css.underline
]
]
[ Html.Styled.text "more grey text" ]
]

When compiled and run in a browser, it will produce the following html structure:

Things to note:

  1. Html.Styled.div seems to be identical to Html.div, except it accepts a css property. However, it does return a different data structure, which is why it must later be converted to "unstyled" HTML.
  2. The generated CSS code is injected into a <style> element in the HTML, as a child element to the root styled HTML element. It contains CSS for all styled elements, not just the top element.
  3. CSS class names are auto generated.
  4. CSS styles are de-duplicated. The CSS for grey text only appears once.

So how does all this work? How do we go from the code we see above, to the resulting HTML structure?

How elm-css works

Let’s explain this code example, starting from the inner most function calls.

Functions like Css.color and Css.textDecoration return a CSS key-value string wrapped in a Style type, that represents the actual CSS property that will later be injected into the HTML structure. Some Style content, like media queries or global selectors, will need further processing before it can be injected into the page.

The css function takes a list of styles and compiles it to CSS source code with one major exception: the class name. Once it has the CSS, this is passed to a hash function which converts it to a 32-bit number. This 32-bit number is hex encoded, and that hex encoded value becomes the class name for this particular list of styles.

For instance, the following Elm code:

[ Css.color <| Css.rgb 125 125 125
, Css.textDecoration Css.underline
]

Will be converted to the following CSS:

{
color:rgb(125, 125, 125);
text-decoration:underline;
}

The resulting hash of this CSS code is 1444442742, and the hex encoded value becomes 56187276. This hex value, will be used as the class name for this particular CSS code.

Once we have figured out the class name, the css function returns a Html.Styled.Attribute type which contains:

  1. An Html.Attribute with our generated class name, this will be injected directly into the final HTML.
  2. The list of styles, which we’ll need to recompile with the generated class name.
  3. The class name, which we’ll use for de-duplicating styles and when recompiling the list of styles.

Moving on.

Html.Styled.div, and similar functions, simply returns a data structure containing its input.

The final piece of the puzzle is Html.Styled.toUnstyled. This function converts the styled HTML elements to unstyled ones. For every element, it collects all matching class names and style lists in a dictionary. When done, it iterates over all the style lists that is in the dictionary and compiles the lists into CSS code using the associated class name. Because this has been stored in a dictionary, the styles have become de-duplicated.

The result of this compilation is then wrapped in a <style> element, and added as the first child of the root element.

Finding potential performance improvements

I’ve tried to summarize the important parts of the elm-css flow visually in the following diagram:

From a performance point of view, there are a couple of interesting things that stand out:

  1. All HTML elements will compile and hash their CSS properties. Got a list of 100 elements with identical CSS? That CSS will be compiled and hashed a 100 times.
  2. CSS will potentially be compiled twice. With and without its class name. Thankfully, due to de-duplication, we only compile a list of styles twice if it’s unique.

Before we decide on how to proceed, we first need to figure out which of the above operations are most expensive. Luckily, I’ve recently done some profiling on a slow Elm application at work, and here are the results:

Judging by the self time value, almost 25% of the duration of this particular profiling session was spent within elm-css's getClassname function. While it isn't clear from the image above, further investigation reveals that several of those (anonymous) function calls are related to the hash function used by elm-css.

It seems, then, that improving the performance of hashing is the most important thing to focus on.

Reducing hashing

A hash function works by converting all the characters of a string into integers, then reduce all those integers into a single integer using some mathematical formulae.

The goal is to come up with a unique id for a string. In practice, this isn’t really possible. You can have an endless combination of characters to form a string, but a hash in JavaScript is often restricted to 32-bits. So you’re bound to have collisions, meaning multiple strings getting the same hash value.

A hash function is considered to be of good quality if it is (a) fast and (b) has a low probability of collisions. elm-css uses the Murmur3 hash function, which in general is considered to be of high quality.

From a performance perspective, it might be better to use the FNV hash function in Elm. However, there’s a bigger chance of getting collisions with that hash function, and a collision means that two unrelated style lists generates the same class name, causing problems.

What we can do is to reduce the size of the compiled CSS. As we’ve seen, elm-css generates code like:

{
color:rgb(125, 125, 125);
text-decoration:underline;
}

This is easy to read, but contains a lot of unnecessary characters that needs to be hashed. There’s no reason we can’t generate more compact code, like this:

{color:rgb(125,125,125);text-decoration:underline;}

This removes a significant amount of characters. In my testing, I saw performance improvements of 10–15% depending on the browser.

Avoiding hashing

Every single time a we define a css property, we compile and hash the CSS in order to get a class name.

Based on our profiling, we know this to be the most expensive part of generating the final HTML structure. For HTML that contains a list of equally styled elements, this means we’re likely to spend a lot of time hashing identical CSS.

But do we really need the class name that early in the process? No CSS or HTML elements are created before toUnstyled is called. If we could delay class name generation to a later stage, we might be able to avoid hashing identical CSS entirely.

Such a change will touch a lot of code. It’s a relatively big restructuring of how elm-css works. So if we're already going to do that, we might also be able to avoid generating CSS more than once.

The first change I made was to the css function. I still let elm-css compile the list of styles into CSS, but with a twist. Instead of leaving the class name out, I add in a placeholder character that can be used to inject the class name using String.replace. I’ll get into more details about this later.

The hashing part of the function was removed.

Instead of returning a Html.Styled.Attribute which contain the list of styles and the class name, I changed the type to only include the compiled CSS. Not the list of styles, as we’re done compiling it, and not the class name, which we’ll generate later.

In the toUnstyled function, I changed the dictionary used for de-duplicating styles to instead contain the generated CSS as the key, and the hash as the value. When iterating over all the HTML elements, we need to check if the CSS for an element is already in the dictionary. If it is, then no action is necessary. If it isn't, we need to insert it along with its hash. This way, we only hash unique CSS, at the cost of more expensive dictionary lookups due to the increased size of the key.

Finally, when iterating the dictionary to extract the CSS styles, we need to call String.replace on the keys, replacing the placeholder character with the associated class name value. This way we avoid generating the CSS twice.

The flow now looks something like this:

An updated visual summary of the inner workings of `elm-css`
An updated visual summary of the inner workings of `elm-css`

On the benchmark that I used I went from 3900 ops/sec all the way up to 6800 ops/sec. That’s nearly double the performance!

Conclusion

I hope you’ve learned a little about how elm-css works. As of today, the PR introducing these changes hasn’t been merged. When (if?) it does, you can expect elm-css to become quite a bit faster.

Update: the changes described in this article have been released as part of version 17.0.2.

--

--