Benchmarking JavaScript: is removeChild faster than innerHTML given thousands of DOM elements?

Motivated by a type ahead challenge in JavaScript30, I explore if removing 2500 DOM nodes is more performant using Element.removeChild or Element.innerHTML. Interesting exercise and results!

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

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 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:

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 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. 🐈

2

Comment via email.