DEV Community

loading...
Cover image for Javascript DOM Manipulation to improve performance

Javascript DOM Manipulation to improve performance

Gabriel Mayta
Keep calm and code!
Updated on ・2 min read

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!

Discussion (29)

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 Author

Spread operator, great, thanks for your comment Fabio.

Collapse
grandemayta profile image
Gabriel Mayta Author

Thanks you, I will update the article.

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
_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
M. Shemayev

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
pheianox profile image
pheianox

Detached tree? What do you mean by that? Can you show some examples?

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

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 Author

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 Author

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 Author

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 Author

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