budu.js

Library to batch DOM reads and writes, reducing layout jank and getting a better performance for animations.

budu.js

Build Status Size JavaScript Style Guide

A bunch of DOM updates.

Don't let the browser throw away the hard work it has done for you.

Library to batch DOM reads and writes to reduce layout thrashing and getting a better performance for animations, visualisations or layout reflows.

Why should I care?

When you change styles the browser checks to see if any of the changes require layout to be calculated, and for that render tree to be updated. Changes to “geometric properties”, such as widths, heights, left, or top all require layout.

In data visualisation or for animation, a developer might want to update a lot of elements. This is possible with the following code:

// let's assume elements is an array of 100 divs we want to animate
elements.forEach(el => {
  // read from the DOM
  const bounds = el.getBoundingClientRect()
  // write to the DOM
  el.style.left = bounds.x + 10
})

This is inefficient as it forces synchronous layout updates. Every time the left style of an element gets updated, the browser has to recalculate the layout before the next call to getBoundingClientRect, triggering the following flow:

A lot of work

read > update > layout > read > update > layout > read > update > layout > read > ...

That leads to a performance bottleneck. To mitigate the problem, it is better to first read all values and after that modify the DOM. Therefore, the browser doesn't have to recalculate layout that much and we get a flow similar to this:

Less work

read > read > read > update > update > update > layout

We reduced layout calls to 1 and with the above code example we would have reduced 100 calls to 1. The code would look like this:

let elementSizes = []
elements.forEach(el => {
  // read from the DOM
  const bounds = el.getBoundingClientRect()
  elementSizes.push(bounds)
})

elements.forEach((el, i) => {
  // write to the DOM
  el.style.left = elementSizes[i].x + 10
})

This was a lot to read, now let's see an example! Press the ▶️ Start button to start the animation. It will run with the default browser behaviour and a lot of synchronous layout calls.

While the animation runs, click on 🐇 Scheduled to see the animation run super smooth. Now all reads happen before all writes.

Play around with the example on CodeSandbox

https://codesandbox.io/s/5z30qplvzk

Be careful

Before rewriting all your code, please actually measure if layout is the problem. The default browser behaviour is usually fine.

You might say "This looks simple, it's 2 loops" and that is correct. In a large app or visualisation, you might update styles in a lot of places. Rewriting all these calls, to execute in batches, is a hassle. This is where budu comes into play. Call budus schedule function and it will coordinate all layout reads and writes in your app for you. The above code example in budu looks like this:

import schedule from 'budu'

elements.forEach(el => {
  schedule({
    measure: () => el.getBoundingClientRect()
    // the return value of `measure()` is available in `update()`
    update: bounds => { el.style.left = bounds.x + 10 }
  })
})

Performance Analysis

The screenshots visualise the work the browser has to do, to animate the example above. Screenshots taken with Chrome Devtools.

Default browser behaviour (forced synchronous layout)

Performance graph with default browser behaviour To render 1 frame the browser needs ~100ms which means it can render at 10 frames per second.

budu batched DOM reads and writes

Performance graph with budu To render 1 frame the browser needs less than 16ms which means it can render at more than 60 frames per second.

Usage

To use budu install it with npm or yarn as follows.

> npm install --save budu
# or
> yarn install budu

After that you can import or require it from your source files.

import schedule from 'budu'
/* or */
const schedule = require('budu')

budu uses window.requestAnimationFrame to schedule measurement and updates. Some older browsers don't support this API. You can still use budu by using a polyfill for requestAnimationFrame like raf/polyfill.

To see if the browser you want to support implements requestAnimationFrame, follow this link -> Can I Use.

API

budu.js provides a default export, which is a function, and nothing else. This function takes an object as only argument, which is used to register a measure and update pair.

import schedule from 'budu'

const task = {
  measure: () => {
    // call all your expensive DOM reads here
    const bounds = element.getBoundingClientRect()
    return bounds
  },
  update: (bounds) => {
    // write to the DOM in here
    element.style.left = bounds.x + 20 + 'px'
  }
}

schedule(task)

function measure () : any

The measure function is used to read values from the DOM with APIs like getBoundingClientRect, getComputedStyle or everything else that triggers layout recalculation. All measure calls will get executed after each other and before the first update function.

The return value of measure is available in it's corresponding update function. Errors in measure will get caught and replace the return value.

function update (val: any) : void

The update function is the right place to update the DOM. It will be called with the result of measure or any Error that could have occured in measure.


I made this mini library to simplify creating performant data visualisations. I learned a lot and took inspiration from the following articles and libraries: