DEV Community

Cover image for Performance of Runtime CSS Generation using MutationObserver
Yeom suyun
Yeom suyun

Posted on

Performance of Runtime CSS Generation using MutationObserver

Recently, I found an interesting tweet on Twitter.

The contents of the commit attached to the above tweet were as follows.

// before
let cssFunctions = ['min', 'max', 'clamp', 'calc']
function isCSSFunction(value) {
  return cssFunctions.some((fn) => new RegExp(`^${fn}\\(.*\\)`).test(value))

// after
let cssFunctions = ['min', 'max', 'clamp', 'calc']
let IS_CSS_FN = new RegExp(`^(${cssFunctions.join('|')})\\(.*\\)`)
function isCSSFunction(value) {
  return IS_CSS_FN.test(value)
Enter fullscreen mode Exit fullscreen mode

I think most people will be able to tell immediately what was changed when they see the original code.
The original code was very inefficiently written.
I would have changed it in the following way, but regardless, the performance of isCSSFunction has been greatly improved through the commit.

let IS_CSS_FN = /^(?:calc|clamp|max|min)\(.*?\)/
function isCSSFunction(value) {
  return IS_CSS_FN.test(value)
Enter fullscreen mode Exit fullscreen mode

Tailwind is still slow

I once attached a benchmark result using a site called PageSpeed Insights while writing an article about the CSS Library I created.

However, this site runs on a local PC, so there is a large deviation every time and the reliability is low.
I found a site called WebPageTest that can get more reliable measurement results.And the following are the numbers measured using this site.
Here's something to keep in mind.
Tailwind is ultimately used in production as a built-in result, so the performance expectation is the same as Atomic CSS, regardless of whether or not jit mode is used.
The Tailwind CDN used in the test is a feature that allows you to try Tailwind using MutationObserver without going through the build stage, and is not suitable for production use.
Recently, I saw an argument in an article titled Tailwind vs Semantic CSS that Tailwind is slow and requires much more markup to compensate for the lack of selector functionality.
This is not true.
So why is tailwind-jit included in the benchmark?
It's because CSS Lube is implemented using the MutationObserver feature, just like Tailwind's Play CDN feature.

Why is CSS Lube fast?

That's the topic of this article.
CSS Lube has a total blocking time of over 2 seconds, which is a bit long, but it is still scoring a speed index score that is almost the same as inline style.
And as you can see from the test page, the test page has 11,186 elements, and the blocking time includes the time it takes to parse the remaining invisible elements outside the first screen after quickly loading the first screen.
This performance was actually possible thanks to the extremely simple logic.

Using RegExp instead of querySelectors

MutationObserver is basically used by creating two.
One detects changes in className, and the other detects new nodes being added.
It is enough to simply iterate through the classList and check if it is a new className and execute compile_style, but it is a bit different in the case of a new node being added.
When CSS Lube detects childList, it first selects the top nodes that do not overlap, and then executes a regular expression on the outerHTML to get the className.

  /** @type {Set<Element>} */
  let elements = new Set
   * @param {Element} target
   * @returns {void}
  let collect_unique_top_nodes = target => {
    if (target.nodeType != 1 || elements.has(target)) return
    for (let e of elements) {
      if (e.contains(target)) return
      if (target.contains(e)) elements.delete(e)

  let get_class_name_regex = /class="([^"]+)"/g
  let get_cname_regex = /\S+/g
   * @param {Element} target
   * @returns {void}
  let detect_childs_with_classes = target => {
    let class_name = ""
    for (let [, substr] of target.outerHTML.matchAll(get_class_name_regex)) {
      class_name += " " + substr
Enter fullscreen mode Exit fullscreen mode

One interesting thing here is that it was much faster to execute get_cname_regex once after connecting the classNames than to execute get_cname_regex for each className.

Previous version problems

Before version 2, CSS Lube used the readystatechange event or the defer attribute on the script tag to use MutationObserver on document.body.
However, I saw that the Master CSS library had a speed index of 2 seconds, which was faster than CSS Lube.
Even though the total blocking time was over 6 to 8 seconds, which was quite bad.
After analyzing the cause, I was able to speed up the speed index from the late 2 seconds to the early 1 second by simply using document.documentElement instead of document.body.

Simple compilation logic

Ultimately, the most important factor for fast performance is simple logic and a simple syntax that makes it possible.
This is also mentioned in my first article attached above.
Basically, CSS Lube can be written the same way as inline styles, and _ is replaced with a space to support spaces.
If the first letter is a special character other than -, the characters before / become the selector.
In addition, if the first letter is @, the previous characters before the next @ become the media query.
The abbreviation function is simply a function that can be used to shorten media queries or some styles.
Through this simple syntax, it was possible to provide powerful styling features without restrictions while still being lightweight and fast, with a size of 2,655 bytes in gzip, half of which is filled with mappings for abbreviations.
CSS Lube


CSS Lube uses MutationObserver to achieve Atomic CSS-level performance without the need for a build step.
It requires no configuration and runs at runtime, so you can manipulate CSS like inline styles using JS, and there are no restrictions on using selectors and media queries.
The concise syntax minimizes the learning curve and improves workflow.
Does all of this sound like hype?
Check it out for yourself in the REPL - CSS Lube.

Thank you.

Top comments (0)