Much Debouncing

The Challenge

In the life cycle of a <much-select> a lot of events are triggered. Events can trigger additional events leading to big cascades of events that can block the main browser thread. We can prevent this from happening with debouncing.

Two example of this in <much-select>:

  1. Each keypress when a user is filtering down a list of options. If we debounce keypress events by a half a second or so the UI looks smoother and we do a lot less work.
  2. Updating the options in the <much-select> following a relevant DOM change. However, DOM changes can come in in rapid succession. Waiting a 10th of a second or so, for changes to stabilize/finish before updating the options saved a lot of work.

Changes and re-renders in the DOM can be costly. Reducing their frequency significantly improves the responsiveness of <much-select> and enables it to manage larger lists of options more efficiently.

The Solution

Debouncing in JavaScript

For this project, I spent more time in various browser developer tools looking at “flame graphs” after recording some activity. The basic idea is that I would look for events/functions called a whole bunch. If they were responding to something the user was doing and were making an expensive call, I could debounce those calls and things would get a lot better.

Here is the JavaScript function I’m using to make debounced functions.

/**
 * Make a debounced function.
 *
 * @param func
 * @param {number} [delay=250]
 * @return {(function(...[*]): void)|*}
 */

const makeDebounceFunc = (func, delay = 250) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      timeoutId = null;
      func(...args);
    }, delay);
  };
};

It’s a little strange because it’s a function that returns a function. Setting up a function as a debounced function requires an extra step (or 2 depending on how you count). The pattern I’ve been following is to make the debounced function, setting it to a “private” instance variable on the instance of MuchSelect in the constructor.

class MuchSelect extends HTMLElement {
  constructor() {
    super();

    /* ... */

	this._callValueCasingWidthUpdate = makeDebounceFunc((width, height) => {
	  this.appPromise.then((app) => {
	    app.ports.valueCasingDimensionsChangedReceiver.send({
	      width,
	      height,
	    });
	  });
    }, 5);

    /* ... */
  }
  /* ... */
}

Debouncing in Elm

I tried a couple different Elm libraries for debouncing. There are a lot of options. I ended up using the simplest one I could find. The example of how it works in the docs is way clearer than anything I’m likely to write.

The Elm Debugger

The Elm Debugger is great. In fact, it’s so great that I had gotten in the habit of always having it on. Turns out, the Elm debugger’s awesomeness is not free. I spent an embarrassingly long time trying to figure out why part of my code was slow, only to learn that the culprit was the Elm debugger.

I ended up making some adjustments so that I could test performance in sandboxes with the Elm debugger turned off. I still had a lot of performance issues to resolve, but an Elm app’s performance characteristics in debug mode can be very different. I guess the good news i, if an Elm app is running well with the debugger on, you probably don’t need to worry about performance… yet.

Where to Debounce?

One interesting aspect of debouncing is where to do it. Sometimes it needs to happen in Elm. But sometimes, like in the case of a MutationObserver, where you need to set up the MutationObserver in JavaScript and pass events in via Elm Ports, I have the option of doing the debouncing in JavaScript or in Elm.

In cases like this, at least for the time being, I’ve favored doing the debouncing in JavaScript. Primarily because sending a whole bunch of events into Elm makes the Elm Debugger a lot less useful. This is not a great reason. I am hopeful that after the code base becomes a bit more stable, I can refactor this code to move the debouncing from JavaScript to Elm.

Elm Wish List - First Class Debouncing

You know what might be nice. Is if the Elm Runtime had debouncing built into it. It seems like it could work a lot like the elm/random. Then it could hide all the extra events that get thrown out the debouncing. It could probably be implemented more efficiently than a Debouncing Elm library could and it would make the Elm debugger more helpful.