DEV Community

Cover image for Javascript DOM Manipulation to improve performance
Gabriel Mayta
Gabriel Mayta

Posted on • Updated on

Javascript DOM Manipulation to improve performance

I wrote this article to show you with examples, how avoid performance issues when develop Web Applications with Vanilla Javascript.

Use selector instead nested elements

// BAD
let menu = document.querySelector('header > nav > ul.menu');

// GOOD
let menu = document.querySelector('.menu');
Enter fullscreen mode Exit fullscreen mode

Avoid DOM manipulations inside loops

// BAD
for (let i = 0; i < 10; i++) {
    document.querySelector('.numbers').innerText += i;
}

// GOOD
let numbers = '';
for (let i = 0; i < 10; i++) {
    numbers += i;
}
document.querySelector('.numbers').innerText = numbers;
Enter fullscreen mode Exit fullscreen mode

Don't use DOM values inside loops

// BAD
let nodes = document.querySelectorAll('.menu-items');
for (let i = 0; i < node.length; i++) { ... }

// GOOD
let nodes = document.querySelectorAll('.menu-items');
const size = nodes.length;
for (let i = 0; i < size; i++) { ... }
Enter fullscreen mode Exit fullscreen mode

Use css classes instead inline styles

// BAD 
let card = document.querySelector('.card');
card.style.width = '400px';
card.style.color = '#f0f0f0';
card.style.marginTop = '10px';

// GOOD 
let card = document.querySelector('.card');
card.style.cssText = ''.concat(
    'width       : 400px;',
    'color       : #f0f0f0;',
    'margin-top  : 10px;'
);

// BETTER 
.card-custom {
    width: 400px;
    color: #f0f0f0;
    margin-top: 10px;
}

let card = document.querySelector('.card');
card.classList.add('card-custom');
Enter fullscreen mode Exit fullscreen mode

Appending DOM is the most expensive operation, choose the right approach

// BAD
let container = document.querySelector('.container');
for (let i = 0; i < 1000; i++) {
    let a = document.createElement('a');
    a.text = `Row N° ${i}`;
    container.appendChild(a);
}

// GOOD
let container = document.querySelector('.container');
let html = '';
for (let i = 0; i < 1000; i++) {
    html = html.concat(`<a>Row N° ${i}</a>`);
}
container.innerHTML = html;

// BETTER
let a = document.createElement('a');
let container = document.querySelector('.container');
for (let i = 0; i < 1000; i++) {
    let cloneA = a.cloneNode(true);
    cloneA.text = `Row N° ${i}`;
    container.appendChild(cloneA);
}

// BETTER^2
let a = document.createElement('a');
let container = document.querySelector('.container');
let documentFragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    let cloneA = a.cloneNode(true);
    cloneA.text = `Row N° ${i}`;
    documentFragment.appendChild(cloneA);
}
container.appendChild(documentFragment);
Enter fullscreen mode Exit fullscreen mode

Use innerHTML only for first rendering and then use DOM methods

// BAD
let container = document.querySelector('.container');
container.innerHTML = '<input type="text" name="example" value="Hi DEVS!">';
container.innerHTML = '<input type="text" name="example" value="Bye DEVS!">';

// GOOD
let container = document.querySelector('.container');
container.innerHTML = '<input type="text" name="example" value="Hi DEVS!">';
let input = container.querySelector('input');
input.value = 'Bye DEVS!';

Enter fullscreen mode Exit fullscreen mode

If you have some tips, please leave your comment.
Keep calm and code!

Top comments (29)

Collapse
 
_developit profile image
Jason Miller 🦊⚛

FWIW DocumentFragment rarely provides much of a performance benefit these days. Also, in most cases imperative DOM construction is much faster than innerHTML and it's variants (insertAdjacentHTML etc), especially since the HTML parser's performance can be severely impacted by browser extensions like ad blockers. The only trick to remember is that when building DOM up imperatively, you should always build a detached tree first, then apped the fully constructed tree to the document.

Collapse
 
alephnaught2tog profile image
Max Cerrina

The only trick to remember is that when building DOM up imperatively, you should always build a detached tree first, then apped the fully constructed tree to the document.

Why? I'm assuming it's because a smaller tree traverses faster, yeah?

Collapse
 
_developit profile image
Jason Miller 🦊⚛

Mutating a disconnected tree is cheaper than a connected one because it has no chance of causing side effects like layout. So you build up the largest possible disconnected tree, then "commit" all those mutations by attaching its root.

Collapse
 
yhorobey profile image
yhorobey • Edited

Newdays DOM is smart enough to not launch reflow after each DOM change.
The trick is "do not ask DOM for values which are not available without reflow".

For example:
You have some container, div, you add to it a couple of other elements and after each addition you ask DOM "what is height of the parent div". Most likelly this will cause a reflow and this is very costly operation.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
_developit profile image
Jason Miller 🦊⚛ • Edited

A detached tree (or disconnected tree) is a DOM tree that hasn't been added to the document yet. Mutating DOM nodes that are not currently appended to a document is extremely cheap.
Example:

Slow Version
Items are appended to the list, which is already "connected" because it has been inserted into document.body:

let list = document.createElement('ul');
document.body.appendChild(list);
for (let i = 0; i < 1000; i++) {
  let item = document.createElement('li');
  item.textContent = `Item ${i}`;
  list.append(item);
}
Enter fullscreen mode Exit fullscreen mode

Fast Version
All we have to change here is to move the document.body.appendChild(list) to the end.
Because items are appended to the list before the list is appended to document.body, building up each item and inserting it into the list is very cheap. Rendering can only happen after the very last line of code executes.

let list = document.createElement('ul');
for (let i = 0; i < 1000; i++) {
  let item = document.createElement('li');
  item.textContent = `Item ${i}`;
  list.append(item);
}
document.body.appendChild(list);  // we just moved this to the end
Enter fullscreen mode Exit fullscreen mode
Collapse
 
anduser96 profile image
Andrei Gatej

For styles, you could also use
Object.assign(elem.style,
{
prop1: “value1”,
/* ... */
})

Thank you for sharing! I’ve certainly learnt a few useful things.

Collapse
 
fabiobiondi profile image
Fabio Biondi

or by using spread syntax {...elem.style, prop1: "val1" }

Collapse
 
grandemayta profile image
Gabriel Mayta

Spread operator, great, thanks for your comment Fabio.

Collapse
 
uppercod profile image
Matias Trujillo

It is convenient to generate the least amount of mutation in the Dom, if you use an object the mutation will be as many properties as the object has, when using a text the mutation will only be one

Collapse
 
anduser96 profile image
Andrei Gatej

It doesn't make much sense to me. In the end, the same changes are applied to the element, no matter if you use an object or text.
Could you please elaborate on your statement ?

Thread Thread
 
uppercod profile image
Matias Trujillo

Hi!, the use of object generates a mutation of the style for each property associated to it, an object of 10 properties generates 10 mutations to the DOM when modifying style, forcing the browser to interpret the style property 10 times, instead using a string you will generate a single mutation to the element and therefore a single interpretation of the style.
This should not be a problem for you, since this should only inconvenience the user experience when multiple elements are modified concurrently in a record time.

Another benefit of the use of string, is that your style does not have dirty properties.

Thread Thread
 
anduser96 profile image
Andrei Gatej • Edited

Wow! Thank you very much! Now it makes sense.
Could you refer me to other resources so I can read more about that? It’s a very interesting topic.

Collapse
 
grandemayta profile image
Gabriel Mayta

Thanks you, I will update the article.

Collapse
 
tobiassn profile image
Tobias SN • Edited

I’d just like to note a few mistakes you’ve made here:

for (let i = 0; i < 10; i++) {
    document.querySelector('.counter').innerHTML += i;
}
Enter fullscreen mode Exit fullscreen mode

will output “0123456789”, since innerHTML is a string, and thus you’re not adding i, but appending it.

let counter = 0;
for (let i = 0; i < 10; i++) {
    counter += i;
}
document.querySelector('.counter').innerHTML = counter;
Enter fullscreen mode Exit fullscreen mode

will however output “45” instead, since both counter and i are numbers.

Also, you’re using innerHTML to insert text in quite a few of your snippets. This is however not recommended, as it goes through the HTML parser even though it doesn’t contain any HTML (Even then, you should use DOM functions to insert elements, not innerHTML). Thus, you should use innerText instead.

Collapse
 
grandemayta profile image
Gabriel Mayta

Hi Tobias,

you're right. I made a mistake. I upgraded the article. Thanks!

Collapse
 
yhorobey profile image
yhorobey

45, not 55

Collapse
 
tobiassn profile image
Tobias SN • Edited

Thanks, I fixed it.

Collapse
 
pinguxx profile image
Ivan

it would be nice to have codepens or similar for the examples, this way we can see it running and you will be able to check if the code you are presenting works or not

The last example:

container.value = 'Bye DEVS!';

you are not updating the input but the container, this will update the input

container.querySelector("input").value = 'Bye DEVS!';

Thanks for sharing

Collapse
 
grandemayta profile image
Gabriel Mayta

You're right. I will create a codepen with the examples. Thanks.

Collapse
 
howardjesstrupanion profile image
howardjesstrupanion

innerHTML is pretty darn fast: browsers have been optimized for its use, because programmers seemed to like it. See codepen.io/jwhooper/pen/GzKwMV

But unless you're building massive DOM structures (scores of thousands of elements), you're unlikely to actually see the difference in performance. I'd rather worry about writing clear, maintainable code.

Collapse
 
grandemayta profile image
Gabriel Mayta

In this case you can use a tiny library like lit-html

Collapse
 
wanoo21 profile image
Ion Prodan

Thanks for these good tips ;)

Collapse
 
grandemayta profile image
Gabriel Mayta

You're welcome

Collapse
 
raslanove profile image
raslanove

Under the "Don't use DOM values inside loops", did you mean querySelectorAll? Because querySelector only returns the first match or null.

Collapse
 
grandemayta profile image
Gabriel Mayta

Hi Raslanove,
Thanks, I upgraded the code.

Collapse
 
pavelloz profile image
Paweł Kowalski

Premature optimization is root of all evil.

I personally use some of those techniques, but first one especially reminded me of this quote. ;-)

Collapse
 
szhou1 profile image
Steve Zhou

cloneNode() is actually slower than html.concat()
jsperf.com/dom-manipulations-szhou

Either way, thanks for your post! Very helpful!

Collapse
 
vitalyche profile image
Vitaly Chernov

No, added one more case (cloneNode + append.apply)
jsperf.com/dom-manipulations-szhou/2, this is the fastest - for Chrome on Mac