school Are you a developer in an agency in the UK, Indonesia or India? Find out more about our free 2 day Progressive Web Apps training .

Houdini: Demystifying CSS

Have you ever thought about the amount of work CSS does? You change a single attribute and suddenly your entire website appears in a different layout. It’s kind of magic in that regard. (Can you tell where I am going with this?!) So far, we – the community of web developers – have only been able to witness and observe the magic. What if we want to come up with our own magic? What if we want to become the magician? Enter Houdini!

The Houdini task force consists of engineers from Mozilla, Apple, Opera, Microsoft, HP, Intel and Google working together to expose certain parts of the CSS engine to web developers. The task force is working on a collection of drafts with the goal to get them accepted by the W3C to become actual web standards. They set themselves a few high-level goals, turned them into specification drafts which in turn gave birth to a set of supporting, lower-level specification drafts. The collection of these drafts is what is usually meant when someone talks about “Houdini”. At the time of writing, the list of drafts is incomplete and some of the drafts are mere placeholders. That’s how early in development of Houdini we are.

The specifications

Worklets

(spec)

Worklets by themselves are not really useful. They are a concept introduced to make many of the later drafts possible. If you thought of Web Workers when you read “worklet”, you are not wrong. They have a lot of conceptual overlap. So why a new thing when we already have workers? Houdini’s goal is to expose new APIs to allow web developers to hook up their own code into the CSS engine and the surrounding systems. It’s probably not unrealistic to assume that some of these code fragments will have to be run every. single. frame. Some of them have to by definition. Quoting the Web Worker spec:

Workers [...] are relatively heavy-weight, and are not intended to be used in large numbers. For example, it would be inappropriate to launch one worker for each pixel of a four megapixel image.

That means web workers are not viable for the things Houdini plans to do. Therefore, worklets were invented. Worklets make use of ES2015 classes to define a collection of methods, the signatures of which are predefined by the type of the worklet. They are light-weight and short-lived.

Paint worklet

(spec)

Status update: First incomplete implementation landed in Chrome Canary behind the “Experimental Web Platform features” flag.

I am starting with this as it is introduces the fewest new concepts. From the spec draft itself:

The paint stage of CSS is responsible for painting the background, content and highlight of an element based on that element’s geometry (as generated by the layout stage) and computed style.

At the current level of the spec, you can create border and background images dynamically. This allows you to do a lot of new things like having a ripple effect on buttons without creating additional DOM elements. Another big advantage of running your code at an element's paint time over using a <canvas> element is that you will know the size of the element you are painting and that you will be aware of the fragments and handle them appropriately.

Wait, what are fragments?

Fragments

I think of elements in the DOM tree as boxes that are laid out by the CSS engine to make up my website. This model is flawed when inline elements come into play. A <span> may need to be wrapped; so while still technically being a single DOM node, it has been fragmented into 2, well, fragments. The spec calls the bounding box of these 2 fragments a fragmentainer. (I am not kidding.)

Back to the Paint Worklet: effectively, your code will get called for each fragment and will be given access to a stripped down <canvas>-like API as well as the styles applied to the element. Eventually, you will even be able to request an “overflow” margin to allow you to draw effects around the element’s boundaries, just like box-shadow.

class {
  static get inputProperties() {
    return ['border-color', 'border-size'];
  }
  paint(ctx, geom, inputProperties) {
    var offset = inputProperties['border-size']
    var colors = inputProperties['border-color'];
    self.drawFadingEdge(
      ctx,
      0-offset[0], 0-offset[0],
      geom.width+offset[0], 0-offset[0],
      color[0]);
    self.drawFadingEdge(
      ctx,
      geom.width+offset[1], 0-offset[1],
      geom.width+offset[1], geom.height+offset[1],
      color[1]);
    self.drawFadingEdge(
      ctx, 0-offset[2],
      geom.height+offset[2], geom.width+offset[2],
      geom.height+offset[2],
      color[2]);
    self.drawFadingEdge(
      ctx,
      0-offset[3], 0-offset[3],
      0-offset[3], geom.height+offset[3],
      color[3]);
  }
  drawFadingEdge(ctx, x0, y0, x1, y1, color) {
    var gradient =
      ctx.createLinearGradient(x0, y0, x1, y1);
    gradient.addColorStop(0, color);
    var colorCopy = new ColorValue(color);
    colorCopy.opacity = 0;
    gradient.addColorStop(0.5, colorCopy);
    gradient.addColorStop(1, color);
  }
  overflow(inputProperties) {
    // Taking a wild guess here. The return type
    // of overflow() is currently specified
    // as `void`, lol.
    return {
      top: inputProperties['border-size'][0],
      right: inputProperties['border-size'][1],
      bottom: inputProperties['border-size'][2],
      left: inputProperties['border-size'][3],
    };
  }
};

Here is a video of a ripple implementation that uses the Paint Worklet API (Demo).

Compositor worklet

Even though the compositor worklet spec has been moved to the WICG and will be iterated on, it’s the one the specs that excites me the most. As you might know, some operations are outsourced to the graphics card of your computer by the CSS engine, although that is dependent on both your grapics card and your device in general. A browser usually takes the DOM tree and, based on specific criteria, decides to give some branches and subtrees their own layer. These subtrees paint themselves onto it (maybe using a paint worklet in the future). As a final step, all these individual, now painted, layers are stacked and positioned on top of each other, respecting z-indices, 3D transforms and such, to yield the final image that is visible on your screen. This process is called “compositing” and is executed by the “compositor”. The advantage of this process is that you don’t have to make all the elements repaint themselves when the page scrolls a tiny bit. Instead, you can reuse the layers from the previous frame and just re-run the compositor with the changed scroll position. This makes things fast. This helps us reach 60fps. This makes Paul Lewis happy.

As the name suggests, the compositor worklet lets you hook into the compositor and influence the way an element’s layer, which has already been painted, is positioned and layered on top of the other layers. To get a little more specific, you can tell the browser that you want to hook into the compositing process for a certain DOM node and can request access to certain attributes like scroll position, transform or opacity. This will force this element on to its own layer and on each frame your code gets called. You can move your layer by manipulating the layers transform and change its attributes (like opacity) allowing you to do fancy-schmancy things at a whopping 60 fps. Here’s a full implementation for parallax scrolling using the compositor worklet.

// main.js
window.compositorWorklet.import('worklet.js')
  .then(function() {
    var animator = new CompositorAnimator('parallax');
    animator.postMessage([
      new CompositorProxy($('.scroller'), ['scrollTop']),
      new CompositorProxy($('.parallax'), ['transform']),
    ]);
  });

// worklet.js
registerCompositorAnimator('parallax', class {
  tick(timestamp) {
    var t = self.parallax.transform;
    t.m42 = -0.1 * self.scroller.scrollTop;
    self.parallax.transform = t;
  }

  onmessage(e) {
    self.scroller = e.data[0];
    self.parallax = e.data[1];
  };
});

My colleague Robert Flack has written a polyfill for the compositor worklet so you can give it a try – obviously with a much higher performance impact.

Layout worklet

(spec)

Again, the specification for this is practically empty, but the concept is intriguing: write your own layout! The layout worklet is supposed to enable you to do display: layout('myLayout') and run your JavaScript to arrange a node’s children in the node’s box. Of course, running a full JavaScript implementation of CSS’s flex-box layout will be slower than running an equivalent native implementation, but it’s easy to imagine a scenario where cutting corners can yield a performance gain. Imagine a website consisting of nothing but tiles á la Windows 10 or a Masonry-style layout. Absolute/fixed positioning is not used, neither is z-index nor do elements ever overlap or have any kind of border or overflow. Being able to skip all these checks on re-layout could yield a performance gain.

registerLayout('random-layout', class {
    static get inputProperties() {
      return [];
    }
    static get childrenInputProperties() {
      return [];
    }
    layout(children, constraintSpace, styleMap) {
        Const width = constraintSpace.width;
        Const height =constraintSpace.height;
        for (let child of children) {
            const x = Math.random()*width;
            const y = Math.random()*height;
            const constraintSubSpace = new ConstraintSpace();
            constraintSubSpace.width = width-x;
            constraintSubSpace.height = height-y;
            const childFragment = child.doLayout(constraintSubSpace);
            childFragment.x = x;
            childFragment.y = y;
        }

        return {
            minContent: 0,
            maxContent: 0,
            width: width,
            height: height,
            fragments: [],
            unPositionedChildren: [],
            breakToken: null
        };
    }
});

Typed CSSOM

(spec)

Typed CSSOM (CSS Object Model or Cascading Style Sheets Object Model) addresses a problem we probably all have encountered and just learned to just put up with. Let me illustrate with a line of JavaScript:

$('#someDiv').style.height = getRandomInt() + 'px';

We are doing math, converting a number to a string to append a unit just to have the browser parse that string and convert it back to a number for the CSS engine. This gets even uglier when you manipulate transforms with JavaScript. No more! CSS is about to get some typing!

This draft is one of the more mature ones and a polyfill is already being worked on. (Disclaimer: using the polyfill will obviously add even more computational overhead. The point is to show how convenient the API is.)

Instead of strings you will be working on an element’s StylePropertyMap, where each CSS attribute has it’s own key and corresponding value type. Attributes like width have LengthValue as their value type. A LengthValue is a dictionary of all CSS units like em, rem, px, percent, etc. Setting height: calc(5px + 5%) would yield a LengthValue{px: 5, percent: 5}. Some properties like box-sizing just accept certain keywords and therefore have a KeywordValue value type. The validity of those attributes could then be checked at runtime.

<div style="width: 200px;" id="div1"></div>
<div style="width: 300px;" id="div2"></div>
<div id="div3"></div>
<div style="margin-left: calc(5em + 50%);" id="div4"></div>
var w1 = $('#div1').styleMap.get('width');
var w2 = $('#div2').styleMap.get('width');
$('#div3').styleMap.set('background-size',
  [new SimpleLength(200, 'px'), w1.add(w2)])
$('#div4')).styleMap.get('margin-left')
  // => {em: 5, percent: 50}

Properties and values

(spec)

Do you know CSS Custom Properties (or their unofficial alias “CSS Variables”)? This is them but with types! So far, variables could only have string values and used a simple search-and-replace approach. This draft would allow you to not only specify a type for your variables, but also define a default value and influence the inheritance behavior using a JavaScript API. Technically, this would also allow custom properties to get animated with standard CSS transitions and animations, which is also being considered.

["--scale-x", "--scale-y"].forEach(function(name) {
document.registerProperty({
    name: name,
    syntax: "<number>",
    inherits: false,
    initialValue: "1"
  });
});

Font metrics

Font metrics is exactly what it sounds like. What is the bounding box (or the bounding boxes when we are wrapping) when I render string X with font Y at size Z? What if I go all crazy unicode on you like using ruby annotations? This has been requested a lot and Houdini should finally make these wishes come true.

But wait, there’s more!

There’s even more specs in Houdini’s list of drafts, but the future of those is rather uncertain and they are not much more than placeholders for ideas. Examples include custom overflow behaviors, CSS syntax extension API, extension of native scroll behavior and similarly ambitious things all of which enable things on the web platform that weren't possible before.

Demos

I have open-sourced the code for the demo (live demo using polyfill) videos I made so you can get a feeling on what working with worklets feels like. I will update the repository with new demos as new APIs are landing in Canary.

If you want to get involved, there’s always the Houdini mailing list.