Leon Fayer (@papa_fire) currently leads engineering at Teaching Strategies. Leon has over two decades of expertise concentrated on architecting and operating complex, web-based systems to withstand crushing traffic (often unexpectedly). Over the years, he's had a somewhat unique opportunity to design and build systems that run some of the most visited websites in the world and has the opinion that nothing really works until it works for at least a million people.
Loops are generally not great from the performance perspective, but often times it’s relatively easy to pinpoint and optimize those inefficient function you wrote and call inside of a loop. But sometimes, especially with JavaScript, the functions causing the performance hit are the ones that are provided to you by language.
Let’s look at the most basic, old school, example of taking an array of items and adding them to your html table.
for (let i = 0; i < items.length; i++) { document.getElementById("listGrid").innerHTML += "<tr><td>" + items[i].name + "</td></tr>"; }
This is pretty straight forward. Iterate through the array and inject each item name into a listGrid
table . There are no special functions to worry about, this loop is based purely on JavaScript-provided functions for doing exactly those things. And yet, there is still room for optimization.
Reduce property lookup overhead
First of all, let’s look at the loop definition itself. The instruction is to iterate from index 0 to last element of the loop, as defined by the array function length.
Problem here is that items.length
property is being accessed on every iteration of the loop, for really no reason. So let’s take it outside the loop.
const itemArrLen = items.length; for (let i = 0; i < itemArrLen; i++) { document.getElementById("listGrid").innerHTML += "<tr><td>" + items[i].name + "</td></tr>"; }
Now, no matter how many elements are in that array, items.length
will only be looked up once.
Reduce DOM access
The other issue with the example code is the injection action itself. document.getElementById
traverses DOM with each loop iteration to find the same listGrid
element every time. Instead, let’s just do the lookup once, store the element in a variable and use it in a loop for injection.
const itemArrLen = items.length; const element = document.getElementById("listGrid"); for (let i = 0; i < itemArrLen; i++) { element.innerHTML += "<tr><td>" + items[i].name + "</td></tr>"; }
Note, the same applies if you’re using jQuery (or other frameworks) instead of raw HTML for DOM manipulation. Remember, no matter how you do DOM lookups – you’re still doing DOM lookups.
And reduce DOM access again
Finally, let’s address the element.innerHTML
property access. What happens now is we’re looking up the `innerHTML` property at every interaction. That’s one problem. The second problem is that we’re changing the property value with every loop tick, which means updating the DOM, which means a potential (re)layout by the browser. Modern browsers are smart not to do too many layouts and batch them (as they are expensive), however it all depends on what else is going on in the page/app. If another piece of code decides to request layout information (e.g. get the height of some element), the browser has no choice but to flush the queue of batched updates. Ouch.
The solution to both problems is simple: use a local variable for all the string concatenation. Once done, touch the DOM only once to update it:
const itemArrLen = items.length; const element = document.getElementById("listGrid"); let newHTML = ''; for (let i = 0; i < itemArrLen; i++) { newHTML += "<tr><td>" + items[i].name + "</td></tr>"; } element.innerHTML += newHTML;