JavaScript30 Series

I started fo­cus­ing much more keenly on ReactJS and JavaScript while I looked for a job. This post is a part of the se­ries ti­tled JavaScript 30’, which be­gan early October 2020.

Motivation

In JavaScript30, we had a type ahead or an au­to­com­plete ex­er­cise. This in­volved an ar­ray of data with a 1000 en­tries. An empty in­put field would ren­der 1000 el­e­ments. Every ad­di­tional in­put would cause the 1000 en­tries to be fil­tered and shown as sug­ges­tions in an un­ordered list node.

I de­cided that I did­n’t want to show 1000 en­tires over and over each time the user would empty their in­put to restart their search:

...
const currentSuggestions = document.querySelector("ul.suggestions");
...
function suggestPlaces() {
const input = this.value
// If input is empty, clear all suggestions.
if (!input) {
removeChildren(currentSuggestions)
return
}
...
}

This would in­volve emp­ty­ing out the un­ordered list, which could be hold­ing any­where be­tween 0 to 1000 child nodes. The removeChildren func­tion looks like so:

/**
* @description Helper function to remove all children of the specified DOM node.
* @param {Node} node
* @returns void
*/

function removeChildren(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}

After wrestling with a bunch of DOM meth­ods and nearly fin­ish­ing the chal­lenge, I watched Wes Bos’ video to see how he had ap­proached it. Turns out, innerHTML and in­ter­po­lated strings that are writ­ten with back­ticks start­ing ES2015. 😅

This got me won­der­ing if my method was faster or slower — per­haps just a sin­gle call to innerHTML would only cause one paint, whereas thou­sands of calls to removeChild might be worse for per­for­mance?

Setup

jsperf.com no longer seems to be on­line and ap­par­ently has had such is­sues in the past. I’m us­ing js­bench.me for the tests.

The DOM is set up with just a sin­gle node:

<ul class="places"></ul>

The JavaScript sets up 2500 el­e­ments, all ran­domly gen­er­ated:

const dom = document.createDocumentFragment();
let places = Array(2500);
places = places.map((place) => {
return {
population: Math.floor(Math.random() * 1000000),
city: Math.random().toString(36).substring(7),
state: Math.random().toString(36).substring(7),
};
});
const placesList = [];
places.forEach((place) => {
const placeItem = document.createElement("li");
const placeName = document.createElement("span");
placeName.classList.add("name");
placeName.textContent = `${place.city},
${place.state}`
;

// Add population stats
const population = document.createElement("span");
population.classList.add("population");
population.innerHTML = place.population;

// Add to li
placeItem.insertAdjacentHTML(
"beforeend",
placeName.outerHTML + population.outerHTML
);

// Add to list of li's
placesList.push(placeItem);
});

dom.append(...placesList);

const placesElement = document.querySelector("ul.places");

placesElement.appendChild(dom);

The ran­dom string gen­er­a­tion is from a StackOverflow an­swer by dou­ble­tap.

You’ll no­tice I’m also us­ing DocumentFragment, which I’ll de­tail in a sep­a­rate blog post. It’s an­other nifty lit­tle trick for a huge per­for­mance boost!

Test cases

We set up two test cases:

// Test case 1
while(placesElement.firstChild) {
placesElement.removeChild(placesElement.firstChild);
}

The first test case sim­ply loops and re­moves the first child of a par­ent node, as long as there is a first child.

// Test case 2
placesElement.innerHTML = '';

The sec­ond test case is very sim­ple - we change the in­ner­HTML to an empty string. As per MDN docs for Element.innerHTML:

Setting the value of in­ner­HTML lets you eas­ily re­place the ex­ist­ing con­tents of an el­e­ment with new con­tent. For ex­am­ple, you can erase the en­tire con­tents of a doc­u­ment by clear­ing the con­tents of the doc­u­men­t’s body at­tribute.

Results

The re­sults were cer­tainly sur­pris­ing at first. Have a look your­self.

Raw per­for­mance

Screenshot of the test runs from jsbench.me showing how many ops/s were run in each test case, and which test case was the fastest by how much.

Test case 1 runs roughly 32.3 mil­lion ops/​s whereas test case 2 runs at a slower 10.3 mil­lion ops/​s — slower by more than 65%!

Repaints

I used Firefox’s per­for­mance DevTools to watch for paint events. In both the test cases, there was only a sin­gle paint event:

Screenshot from Firefox's performance DevTools zoomed in on the timeline where test case 1 ran.

Screenshot from Firefox's performance DevTools zoomed in on the timeline where test case 2 ran.

While the lay­out (3) and paint events (1) oc­curred the same num­ber of times, us­ing the more per­for­mant Element.removeChild method caused twice as many style re­cal­cu­la­tions (4 vs 2) and its ap­pli­ca­tions (2 vs 1) com­pared to us­ing Element.innerHTML.


Of course this will be dif­fer­ent on your hard­ware as well as browser. You may check out the JSBench test suite for your­self. I’m pretty new to bench­mark­ing — let me know if there’s some­thing ob­vi­ous I’m do­ing wrong here. 🐈