A Tip for Testing the Speed of User Interactions in Your Javascript App

Sep 14, 2015

At least one layout (reflow) should occur for every user interaction. Ensure this by triggering user events in your test code with asynchronous callbacks.

Imagine you have some code that appends children to an element when a user clicks another element.

<html>
<body>
  <div id="root"></div>
  <div id="append-child"></div>
  <script>
    var root = document.getElementById('root');

    document.getElementById('append-child').onclick = function() {
      var div = document.createElement('div');
      div.textContent = "hi";

      root.appendChild(div);
    }
  </script>
</body>
</html>

You want to see how fast the DOM is when a user clicks #append-child many times. Don’t do this:

var startTime = new Date();

for (i=0; i < 100; i++) {
  document.getElementById('append-child').click();
}

console.debug("Inaccurate time: " + (new Date() - startTime));

Try this junk measurement.

Instead, do this:

var startTime = new Date();

function appendChildren(count) {
  setTimeout(function() {
    document.getElementById('append-child').click();

    if (count === 100) {
      return console.debug("Accurate time: " +
        (new Date() - startTime));
    } else {
      appendChildren(count + 1);
    }
  }, 0);
}

appendChildren(1);

Try this better measurement.

Consider the first example. Most modern browsers append 100 “hi”s at the same time. They gather DOM writes in synchrnous code, even if triggered by DOM events. Those browsers execute a layout after the code is completely evaluated.

The second example executes a layout every time our simulated user clicks the #add-element element. It gives us a time we can compare to a speed test of another implementation of the same functionality.

Some bad DOM performance occurs when a single user action triggers many layouts. So you’ll want to account for all those layouts in your measurement. Many layouts are executed if you call certain properties on elements between statements that write to the DOM. For example, calling clientHeight on an element between 2 DOM writes will trigger two layouts.

var root = document.getElementById('root');
var child = document.createElement('div');

root.appendChild(child);

var childHeight = child.clientHeight;

child.style.height = (childHeight * 2) + 'px';

To determine the client height, the browser triggers a layout when child.clientHeight is evaluated. Then the browser triggers another layout when the code is done executing.

Further reading