Chapter 3 DOM programming of high performance JavaScript

Chapter 3 DOM programming

High performance JavaScript - Nicholas C. Zakas

Operating DOM with JS is expensive, which is usually the performance bottleneck of web applications.

This chapter discusses three types of issues:

  • Access and modify DOM elements
  • Modifying the style of DOM elements will result in repaint and reflow
  • Handling interaction with users through DOM events

1. DOM in browser

DOM (Document Object Model) is the interface layer for operating XML/HTML defined by W3,
The browser implements the (DOM) interface layer, and we (developers) call the interface layer,
In other words, we can operate the interface through the (DOM) interface layer.

Browser implementation of DOM and ECMAScript are two different things,
For example, Chrome uses the WebCore Library of webkit to render the interface, and V8 to implement ECMAScript.

It can be considered that webkit and v8 are two different islands,
There is additional overhead in their communication.

2. DOM access and modification

Accessing DOM elements comes at a cost,
Modifying elements is more expensive because it usually leads to recalculation of page redrawing and rearrangement.

In particular, loop the HTML element collection as follows:

// Read and write 15000 * 2 times in total
function innerHTMLLoop() {
  for (var count = 0; count < 15000; count++) {
    // Read it first and write it again
    document.getElementById('here').innerHTML += 'a';
  }
}

The number of times to read and write DOM should be reduced as much as possible, and the operation logic should be put into JS as much as possible, as follows:

// Read and write 2 times in total
// After improvement, the performance (time spent) is improved dozens of times
function innerHTMLLoop2() {
  var content = '';

  for (var count = 0; count < 15000; count++) {
    content += 'a';
  }

  document.getElementById('here').innerHTML += content;
}

2.1. innerHTML vs DOM method

There are two ways to modify the page area:

  • innerHTML
  • document.createElement()

The performance of the two methods is almost the same,
In non modern browsers, innerHTML is a little faster,
In modern browsers, document Createelement() is a little faster.

It should be considered comprehensively according to readability, stability, team habits and code style.

2.2. Clone node

Create a new page, except document createElement(tagName),
You can also use VaR dupnode = node cloneNode(deep); Clone an existing node.

Although cloning nodes will be more efficient, it is not particularly obvious, and can only bring a few percent performance improvement.

2.3. HTML collection

The HTML collection is a class array object. The elements in the collection are references to Node nodes. The collection method is as follows:

  • document.getElementsByName()
  • document.getElementsByClassName()
  • document.getElementsByTagName()
  • document.images All img elements on the page
  • document.links All a elements
  • document.forms All forms
  • document.forms[0].elements All fields in the first form on the page
  • wait

They all return HTMLCollection objects.

HTML collections are real-time,
That is, when the underlying document object is updated, it will also be updated automatically.

The HTML collection object remains connected to the document,
For example, length, each time the number of elements of the collection object is obtained, it will be queried again on the whole page to obtain the latest number.

2.3.1. Expensive collection

The collection is real-time, with the following code:

var alldivs = document.getElementsByTagName('div');

// Every time you execute alldivs length 
// Will be executed again ` document GetElementsByTagName ('div ') ` get the latest length
for (var i = 0; i < alldivs.length; i++) {
  // Insert new elements into the page
  document.body.appendChild(document.createElement('div'))
}

The code above is an endless loop.

Compare the following operations:

// slow
// Every time you access length, you will re query all div s
function loopCollection() {
  var coll = document.getElementsByTagName('div');

  for (var count = 0; count < coll.length; count++) {
    /* do nothing */
  }
}

// fast
// Cache length is several times to dozens of times faster than the above code
function loopCacheLengthCollection() {
  var coll = document.getElementsByTagName('div'),
      len = coll.length;

  for (var count = 0; count < len; count++) {
    /* do nothing */
  }
}


function toArray(coll) {
  for (var i = 0, a = [], len = coll.length; i < len; i++) {
    a[i] = coll[i];
  }
  return a;
}

// fast
// Copying collection elements to an array is a little faster than the above code, because accessing array elements is faster than accessing class array elements
function loopCopiedArray() {
  var coll = document.getElementsByTagName('div');
  var arr = toArray(coll);

  for (var count = 0; count < arr.length; count++) {
    /* do nothing */
  }
}

2.3.2. Cache collection elements using local variables

When traversing the HTML collection, the optimization principles are as follows:

  • Cache collection objects: store collections in local variables
  • Cache length attribute: cache length outside the loop
  • Cache collection elements: put multiple accessed collection elements into local variables

The code is as follows:

// slow
function collectionGlobal() {
  var coll = document.getElementsByTagName('div'),
      len = coll.length,
      name = '';

  for (var count = 0; count < len; count++) {
    name = document.getElementsByTagName('div')[count].nodeName;
    name = document.getElementsByTagName('div')[count].nodeType;
    name = document.getElementsByTagName('div')[count].tagName;
  }

  return name;
};

// Faster
// Several to dozens of times faster than the above code, cache the collection object and cache the length attribute
function collectionLocal() {
  var coll = document.getElementsByTagName('div'),
      len = coll.length,
      name = '';

  for (var count = 0; count < len; count++) {
    name = coll[count].nodeName;
    name = coll[count].nodeType;
    name = coll[count].tagName;
  }

  return name;
};

// Fastest
// A little faster than the above code, cache the collection object, cache the length attribute, and cache the collection elements accessed multiple times
function collectionNodesLocal() {
  var coll = document.getElementsByTagName('div'),
      len = coll.length,
      name = '',
      el = null;

  for (var count = 0; count < len; count++) {
    el = coll[count];
    name = el.nodeName;
    name = el.nodeType;
    name = el.tagName;
  }

  return name;
};

2.4. Traversing DOM

DOM API provides many ways to access the document structure of a specified part. You can choose an efficient way according to the situation.

2.4.1. Traverse child nodes

childNodes and nextSibling can be used to traverse child nodes. The code is as follows:

function testNextSibling() {
  var el = document.getElementById('mydiv'),
      ch = el.firstChild,
      name = '';
  
  do {
    name = ch.nodeName;
    console.log(name);
  } while (ch = ch.nextSibling);
}

function testChildNodes() {
  var el = document.getElementById('mydiv'),
      ch = el.childNodes,
      len = ch.length,
      name = '';

  for (var count = 0; count < len; count++) {
    name = ch[count].nodeName;
    console.log(name);
  }
}

The execution speed of the above code is almost the same.

2.4.2. Element node

div, p, a, and so on are all Element nodes

The Element Node inherits the Node node,
Text nodes and annotation nodes also inherit Node nodes.

Sometimes, we need to eliminate non Element nodes. DOM provides some methods

Return Element nodeReturn Node
childrenchildNodes
childElementCountchildNodes.length
firstElementChildfirstChild
lastElementChildlastChild
nextElementSiblingnextSibling
previousElementSiblingpreviousSibling

The method to return the Element node is faster because there are fewer non Element nodes

2.4.3. Selector API

Finding DOM elements through CSS selectors is faster than native DOM methods, as follows:

var errs = [],
    divs = document.getElementsByTagName('div'),
    classname = '';

for (var i = 0, len = divs.length; i < len; i++) {
  classname = divs[i].className;

  if (classname === 'notice' || classname === 'warning') {
    errs.push(divs[i]);
  }
}

// Several times faster than above
var errs = document.querySelectorAll('div.warning, div.notice');

querySelectorAll() returns the NodeList object (snapshot),
It is not an HTML collection, so it does not correspond to the real-time document structure.

3. Redrawing and rearrangement

After downloading page resources (HTML, JS, CSS, pictures, etc.), the browser will parse and generate two internal structures:

  • DOM tree: represents the page structure
  • Render tree: indicates how DOM nodes are displayed

The nodes to be displayed (non hidden nodes) in the DOM tree have corresponding nodes in the rendering tree.

The nodes in the rendering tree are called frames or boxes,
Like CSS box models, boxes include padding, margins, borders, and position,

Once the DOM tree and rendering tree are built, the browser will display (draw paint) the page elements.

When the change of DOM element affects its geometry (width and height),
The browser recalculates the geometry and position of the element and other elements affected by it,
And invalidate the affected part of the rendering tree and reconstruct the rendering tree. This process is called reflow,
After rearrangement, the browser will redraw the affected part to the screen. This process is called repaint.

If the element changes the background color (without affecting its width and height), you only need to redraw it.

Redrawing and rearrangement are very expensive and can lead to slow UI response, so minimize such processes.

3.1. When does the rearrangement occur

Rearrangement occurs when the page layout or element geometry changes:

  • Add or delete visible elements
  • Change element position
  • Change element size (margin, padding, border thickness, width, height)
  • Change element content (change text, change picture size)
  • Page initialization rendering
  • Change browser window size

The rendering tree corresponding to the changed part will be recalculated,
Some changes can cause the entire page to rearrange, such as the appearance of a scroll bar.

3.2. Queueing and refreshing

Since each rearrangement consumes computing power, the browser puts multiple "changes" into the queue and executes them in batch.
That is, create a buffer, cache the "change", perform the calculation after the buffer is full, and finally rearrange it.

However, when you query the layout information, it will cause the buffer to be refreshed (let the "changes" in the buffer be rearranged immediately):

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle() (currentStyle in IE)

The above properties and methods need to return the latest layout information.

So try to put these operations last, that is, delay access to layout information.

3.3. Minimize redrawing and rearrangement

Merge all changes and process them at once:

3.3.1. Change style

var el = document.getElementById('mydiv');

// Treatment alone affects performance
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';

// Merge processing
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

// Or set the style class
el.className = 'active';

3.3.2. Batch processing DOM changes

When you want to modify a DOM element many times, you can reduce rearrangement and redrawing by following the steps below:

  1. Detach elements from document flow
  2. Modify the operation
  3. Returns the element to the document

After this process, only step 1 and step 2 will be rearranged,
If you don't do much, each change in step 2 may cause rearrangement.

There are three basic methods:

  1. Hide the element, then modify the element, and finally display it
  2. Working with document fragments
  3. Clone the element, then modify the cloned element, and finally replace the original element with the cloned element

Example:

function appendDataToElement(appendToElement, data) {
  var a, li;
  for (var i = 0, max = data.length; i < max; i++) {
    a = document.createElement('a');
    a.href = data[i].url;
    a.appendChild(document.createTextNode(data[i].name));
    li = document.createElement('li');
    li.appendChild(a);
    appendToElement.appendChild(li);
  }
};

var data = [
  {
    "name": "Nicholas",
    "url": "http://nczonline.net"
  },
  {
    "name": "Ross",
    "url": "http://techfoolery.com"
  }
];

// Mode 1
var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

// Mode 2
var fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
document.getElementById('mylist').appendChild(fragment); // Only all child nodes of the clip are added

// Mode 3
var old = document.getElementById('mylist');
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
old.parentNode.replaceChild(clone, old);

3.4. Cache layout information

Get layout information (such as offsets, scroll values, computed style values)
It will be a rearrangement buffer refresh (calculate all "changes" in the queue and rearrange).

If the layout information is obtained multiple times, it can be cached in local variables:

// inefficient
myElement.style.left = 1 + myElement.offsetLeft + 'px';
myElement.style.top = 1 + myElement.offsetLeft + 'px';
if (myElement.offsetLeft >= 500) {
  stopAnimation();
}

// Efficient
var current = myElement.offsetLeft;
current++;
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (current >= 500) {
 stopAnimation();
}

3.5. Detach animation elements from the document flow

Elements are usually displayed / hidden in an expanded / collapsed (animated) way on the page,
If not handled well, it will lead to the rearrangement of large areas (even the whole page).

The rearrangement of large areas of the page can be avoided in the following ways:

  1. Separating elements from the document flow by absolute positioning
  2. Apply animation effects to elements
  3. Restores the positioning properties of an element

3.6. Event delegation

Avoid binding event handlers on "same type" elements one by one.

Each event has three phases:

  1. Capture (root element - > target element)
  2. Trigger on target element
  3. Bubbling (target element - > root element)

When an event occurs, the event is propagated to the ancestor element.

Bind the event processor on the ancestor element, and judge whether the target element is the specified element in the processor:

// Prevent all links from opening the page directly
document.onclick = function(e) {
  var target = e.target;

  if (target.nodeName !== 'A') {
    return;
  }

  e.preventDefault();

  handleUnsafeLinks(target.href);
};

4. Summary

In modern web applications, operating DOM is a very important part, and there will be overhead every time you communicate with DOM through JS,
To reduce the performance overhead of DOM programming, remember the following:

  • Minimize DOM operation, try to finish business logic in JS, and then operate dom
  • Use local variables to store frequently accessed DOM references
  • The HTML collection is real-time. Cache the length of the HTML collection, or copy it to the array, and then operate on the array
  • Try to use more efficient API s, such as querySelectorAll(), firstElementChild()
  • Pay attention to rearrangement and redrawing; Batch process style updates, operate the DOM tree "offline", cache and minimize access to layout information
  • Absolute positioning of animation elements, using drag proxy
  • Use event delegates to minimize the binding of event handlers

Keywords: Javascript Optimize

Added by lifeson2112 on Mon, 03 Jan 2022 22:42:46 +0200