Why does manipulating the DOM take time

thread

First of all, we need to know that browsers are multi-process architecture. We can think of each page as a renderer process, and the renderer process is multi-threaded, including JavaScript engine threads, rendering engine threads, etc.

Rendering engine threads and JavaScript engine threads are mutually exclusive, meaning that only one engine thread is running at any one time and the other is blocked. In addition, in the process of thread switching, the state information of the previous thread needs to be saved and the state information of the next thread needs to be read, which is context switching. This operation is time-consuming.

const times = 1000000;
console.time('dom')
for(let i=0; i<times; i++){
  let temp = document.body	// Loop through the body element
}
console.timeEnd('dom')		Dom: / / 25.093994140625 ms


console.time('object')
let obj = document.body		// Save the body as a js variable
for(let i=0; i<times; i++){
  let temp = obj
}
console.timeEnd('object')	/ / object: 2.39892578125 ms
Copy the code

For example, in the above code, each loop read involves a context switch in the first example, which takes longer than the second example involving only JavaScript engine threads

To render

When manipulating the DOM, changes to elements and styles will cause the rendering engine to re-render, making backflow and redrawing possible. Specific can see the browser render the article. Here are the definitions of reflux and redraw

Backflow occurs when we trigger geometry changes in the DOM. Backflow is triggered if any of the following operations occur:

  • Add, delete, or move DOM nodes, for example, display
  • The geometric attributes of DOM nodes are modified. Common geometric attributes include width, height, margin, padding, border and font-size
  • When reading and writing offset, Client, or Scroll attributes, the browser will perform backflow to obtain accurate values
  • Execute the window.getComputedStyle method

Redraw is triggered when we cause a change in the style of the DOM node, but not in the geometry.

  • Changes to color styles, such as color, border-style, backgro0und-color, etc
  • Hide nodes from styles without changing the DOM structure, such as visibility

Take a chestnut

Chrome provides performance analysis tools to analyze the difference in rendering time between backflow and redraw

In the first example, we trigger backflow by modifying the margin of the element after clicking the button.

const times = 100000;
let html = '<button id="btn">change</button>'
for(let i=0; i<times; i++){
  html += `<li>${i}</li>`
}
document.body.innerHTML = html;

const btn = document.getElementById('btn')
btn.addEventListener('click'.() = > {
  const lis = document.querySelectorAll('li')
  lis.forEach((item, index) = > {
    item.style.margin = index % 2 ? '20px' : '5px'})})Copy the code

In the second example, we trigger a redraw by changing the color of the element after clicking the button.

const times = 100000;
let html = '<button id="btn">change</button>'
for(let i=0; i<times; i++){
  html += `<li>${i}</li>`
}
document.body.innerHTML = html;

const btn = document.getElementById('btn')
btn.addEventListener('click'.() = > {
  const lis = document.querySelectorAll('li')
  lis.forEach((item, index) = > {
    item.style.color = index % 2 ? 'blue' : 'red'})})Copy the code

From the two results, the Rendering time of reflow (Rendering + Painting) is significantly higher than that of repainting

How can I manipulate the DOM efficiently

Batch operation element

Operations on elements are executed in batches together

console.time('time')
let body = document.body
for(let i=0; i<3000; i++){
  body.innerHTML += `<div></div>`
}
console.timeEnd('time')		/ / time: 4684.2490234375 ms
Copy the code

For example, if you create 3000 elements, you can use variables to save and then batch update all the elements to the target element

console.time('time')
let html = ""
for(let i=0; i<3000; i++){
  html += `<div></div>`
}
document.body.innerHTML = html
console.timeEnd('time')		/ / time: 2.4091796875 ms
Copy the code

Instead of modifying the innerHTML directly, you can create a container to hold the elements, and then add the container to the target element. The DocumentFragment can therefore be used to create containers that are not part of the real DOM and whose changes do not trigger DOM re-rendering

let content = document.createDocumentFragment()
for(let i=0; i<3000; i++){
  let cur = document.createElement('div')
  cur.innerText = i
  content.appendChild(cur)
}
document.body.appendChild(content)
Copy the code

Cache element variables

Caching element variables is the same as reducing context switches.

let html = ' '
for(let i=0; i<10000; i++){
  html += `<div></div>`
}
document.body.innerHTML = html;

console.time('time')
for(let i=0; i<document.querySelectorAll('div').length; i++){
  document.querySelectorAll('div')[i].innerText = i
}
console.timeEnd('time')		/ / time: 3018.7900390625 ms
Copy the code

For example, in this example, each loop will fetch the selector. So we can use a variable to cache these selectors

let html = ' '
for(let i=0; i<10000; i++){
  html += `<div></div>`
}
document.body.innerHTML = html;

console.time('time')
let query = document.querySelectorAll('div')
for(let i=0; i<query.length; i++){
  query[i].innerText = i
}
console.timeEnd('time')		/ / time: 7.298828125 ms
Copy the code

Avoid using style too often

Not only does frequent use of style involve frequent manipulation of the DOM, it also takes more time to trigger a style evaluation than using class.

Here’s an example of how style is used frequently, and again, we use Perform to record performance after pressing the button

let html = 
for(let i=0; i<100000; i++){
  html += `<div id="i${i}">${i}</div>`
}
document.body.innerHTML = html

document.getElementById('btn').addEventListener('click'.function a(){
  for(let i=0; i<100000; i++){
    let cur = document.getElementById(`i${i}`)
    cur.style.height = '30px';
    cur.style.color = 'red';
    cur.style.border = '1px';
    cur.style.margin = '1px';
    cur.style.padding = '1px'; }})Copy the code

At this point, we use class instead of style to observe the performance difference.

let html = 
for(let i=0; i<100000; i++){
  html += `<div id="i${i}">${i}</div>`
}
document.body.innerHTML = html

document.getElementById('btn').addEventListener('click'.function a(){
  for(let i=0; i<100000; i++){
    document.getElementById(`i${i}`).className = 'cls'}})Copy the code

From the above changes, we can see that JavaScript time and Rendering time are reduced.

In particular, Rendering takes less time to calculate styles, and no difference is made in layout and layer tree updating.