Contents
Motivation
In JavaScript30, we had a type ahead or an autocomplete exercise. This involved an array of data with a 1000 entries. An empty input field would render 1000 elements. Every additional input would cause the 1000 entries to be filtered and shown as suggestions in an unordered list node.
I decided that I didn’t want to show 1000 entries over and over each time the user would empty their input 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 involve emptying out the unordered list, which could be holding anywhere between 0 to 1000 child nodes. The removeChildren
function 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 methods and nearly finishing the challenge, I watched Wes Bos’ video to see how he had approached it. Turns out, innerHTML
and interpolated strings that are written with backticks starting ES2015. 😅
This got me wondering if my method was faster or slower — perhaps just a single call to innerHTML
would only cause one paint, whereas thousands of calls to removeChild
might be worse for performance?
Setup
jsperf.com no longer seems to be online and apparently has had such issues in the past. I’m using jsbench.me for the tests.
The DOM is set up with just a single node:
<ul class="places"></ul>
The JavaScript sets up 2500 elements, all randomly generated:
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 random string generation is from a StackOverflow answer by doubletap.
You’ll notice I’m also using DocumentFragment
, which I’ll detail in a separate blog post. It’s another nifty little trick for a huge performance boost!
Test cases
We set up two test cases:
// Test case 1
while (placesElement.firstChild) {
placesElement.removeChild(placesElement.firstChild);
}
The first test case simply loops and removes the first child of a parent node, as long as there is a first child.
// Test case 2
placesElement.innerHTML = "";
The second test case is very simple – we change the innerHTML to an empty string. As per MDN docs for Element.innerHTML:
Setting the value of innerHTML lets you easily replace the existing contents of an element with new content. For example, you can erase the entire contents of a document by clearing the contents of the document’s body attribute.
Results
The results were certainly surprising at first. Have a look yourself.
Raw performance
Test case 1 runs roughly 32.3 million ops/s whereas test case 2 runs at a slower 10.3 million ops/s — slower by more than 65%!
Repaints
I used Firefox’s performance DevTools to watch for paint events. In both the test cases, there was only a single paint event:
While the layout (3) and paint events (1) occurred the same number of times, using the more performant Element.removeChild
method caused twice as many style recalculations (4 vs 2) and its applications (2 vs 1) compared to using Element.innerHTML
.
Of course this will be different on your hardware as well as browser. You may check out the JSBench test suite for yourself. I’m pretty new to benchmarking — let me know if there’s something obvious I’m doing wrong here. 🐈