Warning: Even this one seems a little long… It may take you 10+ minutes. If you don’t have that much time, you can go directly to the end of the article to read the little sister’s knot.

Scripting DOM manipulation is expensive and is the most common performance bottleneck in rich Web applications. There are three main problems:

  1. Access and modify DOM elements

  2. Modifying the styling of DOM elements results in repaint and reflow

  3. Interact with the user through DOM event handling

DOM in the browser

DOM is a language-independent Application Program Interface for manipulating XML and HTML documents. Although the DOM is language-independent, the interface in the browser is implemented in JavaScript.

A little front-end knowledge

Browsers typically implement JS and DOM separately. For example, in IE, the js implementation named JScript is located in the jscript.dll file. The DOM implementation resides in another library, called mshtml.dll (Trident). DOM in Chrome is implemented as webCore in WebKit, but the JS engine is Google’s own V8. The JAVASCRIPT engine in Firefox is SpiderMonkey, and the rendering engine (DOM) is Gecko.

DOM is by nature slow

As mentioned in the previous tip, the browser implements the page rendering part and the JS part separately. Since it is separate, once the two need to create a connection, there is a price to pay. Two examples:

  1. Xiao Ming and Xiao Hong are students from two different schools. Both of them have poor economic conditions and can’t afford a mobile phone. “, so they can only communicate with each other by letter, which is certainly more costly (extra events, the cost of writing letters, etc.) than if they had talked to each other face to face.

  2. Official example: Think of DOM and JS (ECMAScript) as islands, each connected by a toll bridge. Every time ECMAScript accesses the DOM, it passes over the bridge and pays a “toll.” The more times you access the DOM, the higher the cost.

Therefore, the recommended course of action is to minimize the number of bridge crossings and try to stay on ECMAScript Island.

DOM access and modification

While there is a “bridge fee” to access the DOM, modifying DOM elements is more expensive because it causes the browser to recalculate the geometry of the page. Take a look at this code:



function innerHTMLLoop(a){  
    for (var count = 0; count < 15000; count++){  
        document.getElementById('text').innerHTML += 'dom'; }}Copy the code

In this code, the loop accesses the specific element twice: the first time it reads the innerHTML property of the element, and the second time it overrides it. With this in mind, it’s not hard to come up with a more efficient version:



function innerHTMLLoop2(a){  
    var content = ' ';  
    for (var count = 0; count < 15000; count++){  
        content += 'dom';  
    }  
    document.getElementById('text').innerHTML += content;  
}Copy the code

Use a local variable wrapper for each update, wait for the loop to end, and write to the page once (as much work as possible to the JS part). In all browsers, the modified version is statistically faster (the most significant improvement is IE8, which is 273 times faster).

Collection of HTML elements

A collection of HTML elements is an array-like object that contains references to DOM nodes. You can get a collection of HTML elements using the following methods or attributes:

  • document.getElementsByName()

  • document.getElementsByTagName()

  • document.getElementsByClassName()

  • All img elements in the Document.images page

  • All a elements in the Document.links page

  • All form elements in a Document. forms page

  • Document.forms [0].elements all fields of the first form on the page

The collection of HTML elements is in a “live state,” meaning that when the underlying document object is updated, it is also automatically updated; that is, the connection maintained between the collection of HTML elements and the underlying document object. Because of this, every time you try to retrieve some information from a collection of HTML elements, a query is generated, which is a source of inefficiency.

Expensive set



// This is an endless loop
Believe it or not, I believe it
var alldivs = document.getElementsByTagName('div');  
for (var i = 0; i < alldivs.length; i++){  
    document.body.appendChild(document.createElement('div'));  
}Copy the code

At first glance, this code simply doubles the number of divs on the page: iterating through all of them, creating new ones at a time and adding them to the body. But in reality, this is an infinite loop: the loop exit condition, Alldivs.length, increases after each loop, because the collection of HTML elements reflects the real-time state of the underlying document elements. Next, let’s do something with a collection of HTML elements:



function toArray(coll){  
    for (var i = 0, a = [], len = coll.lengthl i < len; i++){  
        a[i] = coll[i];  
    }  
    return a;  
}  
  
// Copy a collection of HTML elements into an array
var coll = document.getElementsByTagName('div');  
var arr = toArray(coll);  Copy the code

Now compare the following two functions:



function loopCollection(a){  
    for (var count = 0; count < coll.length; count++){  
        //processing...  }}function loopCopiedArray(a){  
    for (var count = 0; count < arr.length; count++){  
        //processing...  }}Copy the code

In IE6, the latter is 114 times faster than the former; IE7 119 times; IE8 79 times… So, with the same content and quantity, iterating through an array is significantly faster than iterating through a collection of HTML elements. Since reading the length attribute of the collection of elements causes the collection to be updated with each iteration loop, this is an obvious performance issue in all browsers, so you can also do this:



function loopCacheLengthCollection(a){  
    var coll = document.getElementsByTagName('div'),  
        len = coll.length;  
    for (var count = 0; count < len; count++){  
        //processing...  }}Copy the code

This function is as fast as loopCopiedArray() above.

Use local variables when accessing collection elements

In general, for any type of DOM access, it is best to use a local variable to cache the member when the same DOM property or method needs to be accessed more than once. When iterating through a collection, the first optimization principle is to store the collection in local variables, cache length outside the loop, and then use local variables to access the elements that need to be accessed multiple times. A chestnut that accesses three attributes of each element in a loop.



function collectionGlobal(a){  
    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;  
        // Omg no one can really write that...
    }  
    return name;  
}Copy the code

Don’t take this code seriously… I’m sure a normal person couldn’t write it… I’m just going to compare this, so I’m going to write down the slowest case. Next, here’s a slightly optimized version:



function collectionLocal(a){  
    var coll = document.getElementsByTagName('div'),  
        len = coll.length,  
        name = ' ';  
    for (var count = 0; count < length; count++){  
        name = coll[count].nodeName;  
        name = coll[count].nodeType;  
        name = coll[count].tagName;  
    }  
    return name;  
}Copy the code

This time it looks a lot more normal, and here’s the final version of the optimized tour:



function collectionNodesLocal(a){  
    var coll = document.getElementsByTagName('div'),  
        len = coll.length,  
        name = ' ',  
        ele = null;  
    for (var count = 0; count < len; count++){  
        ele = coll[count];  
        name = ele.nodeName;  
        name = ele.nodeType;  
        name = ele.tagName;  
    }  
    return name;  
}Copy the code

Traversing the DOM

Crawl through the DOM

Usually you need to start with a DOM element, manipulate the surrounding elements, or recursively find all the child nodes. Consider the following two equivalent chestnuts:



/ / 1
function testNextSibling(a){  
    var el = document.getElementById('mydiv'),  
        ch = el.firstChild,  
        name = ' ';  
    do {  
        name = ch.nodeName;  
    } while (ch = ch.nextSibling);  
    return name;  
}  
  
/ / 2
function testChildNodes(a){  
    var el = document.getElementById('mydiv'),  
        ch = el.childNodes,  
        len = ch.length,  
        //childNodes is a collection of elements, so the length attribute is cached in the loop to avoid iterative updates
        name = ' ';  
    for (var count = 0; count < len; count++){  
        name = ch[count].nodeName;  
    }  
    return name;  
}Copy the code

In different browsers, the running time of the two methods is almost equal. But in older versions of Internet Explorer, nextSibling performs a little better than childNodes.

Element nodes

As we know, DOM nodes have the following five categories:

  • The entire document is a document node

  • Each HTML element is an element node

  • The text inside an HTML element is a text node

  • Each HTML attribute is an attribute node

  • Comments are comment nodes

DOM attributes such as childNodes, firstChild, and nextSibling do not distinguish between element nodes and other types of nodes, but often we only need to access element nodes, which requires some filtering. In fact, these types checking procedures are unnecessary DOM operations. Many modern browsers provide apis that only return element nodes, and it is recommended to use these apis directly if available, as they perform more efficiently than their own filtering in JS.

  1. Apis provided by modern browsers (replaced apis)

  2. children(childNodes)

  3. childElementCount (childNodes.length)

  4. firstElementChild (firstChild)

  5. lastElementChild (lastChild)

  6. nextElementSibling (nextSibling)

  7. previousElementSibling (previousSibling)

With these new apis, element nodes can be retrieved directly and, as a result, faster.

The selectors API

Sometimes developers have to combine calls to getElementById, getElementsByTagName, and walk through the returned nodes in order to get the desired list of elements, but this cumbersome process is inefficient. The latest browsers provide a native DOM method called querySelectorAll() that passes a CSS selector. This is naturally much faster than using JS and DOM to find elements by walking through them. For instance,



var elements = document.querySelectorAll('#menu a');Copy the code

This code returns a NodeList———— class array object containing matching nodes. Unlike before, this method does not return a collection of HTML elements, so the returned nodes do not correspond to the real-time document structure, and it avoids the performance (and potential logic) problems previously caused by HTML collections. Instead of using querySelectorAll(), we need to write:



var elements = document.getElementById('menu').getElementsByTagName('a');Copy the code

Not only is it more difficult to write, but notice that elements is a collection of HTML elements, so you need to copy it into an array to get a static list that looks like elements. There is also a querySelector() method that gets the first matched node.

Repaints & Reflows

The browser displays all the “components” of a page, including HTML tags, JS, CSS, images — and then parses and generates two internal data structures:

  • DOM tree (representing page structure)

  • Render tree (showing how DOM nodes should be represented)

Each node in the DOM tree that needs to be displayed has at least one corresponding node in the render tree. The nodes in the render tree are called frames or boxes, and the CSS box model defines a page element as a box with padding, margin, borders, and position. Once the render tree is built, the browser starts displaying page elements, a process called paint.

When a DOM change affects an element’s geometry (width, height) – such as changing the border width or adding text to a paragraph that increases the number of lines – the browser needs to recalculate the element’s geometry, as well as the geometry and position of other elements on the page. The browser will remove the affected parts of the render tree and rebuild it, a process called “reflow.” After the rearrangement is complete, the browser redraws the affected parts into the browser, a process called repaint.

If you change something other than the element’s geometry, such as changing the element’s background color, there is no rearrangement, only one redraw, because the element’s layout has not changed. Both redrawing and rearranging are expensive operations that cause the UI of your Web application to become unresponsive, and should be minimized as much as possible.

When does the rearrangement take place?

  • Add or remove visible DOM elements

  • Element position changes

  • Element size changes (padding, margin, border, height, width)

  • Content changes (text changes or image sizes change)

  • The page renderer is initialized

  • Browser window size changed

  • The appearance of a scrollbar (which triggers a rearrangement of the entire page)

Minimize redraw and rearrange

Change the style

A chestnut:



var el = document.getElementById('mydiv');  
el.style.borderLeft = '1px';  
el.style.borderRight = '2px';  
el.style.padding = '5px';  Copy the code

In the example, three styles of the element are changed, and each affects the element’s geometry. In the worst case, this code triggers three rearrangements (most modern browsers are optimized for this and only trigger one). On the other hand, this code accesses the DOM four times and can be optimized.



var el = document.getElementById('mydiv');  
// Merge all changes and do it once
//method_1: Use the cssText attribute
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';  
  
//method_2: Changes the class name
el.className = 'anotherClass';  Copy the code

Batch modify DOM

When you need to perform a series of operations on a DOM element, follow these steps:

  1. Takes the element out of the document flow

  2. Apply multiple changes to it

  3. Bring the element back into the document

In the above combo, moves 1 and 3 trigger a rearrangement, respectively. But if you ignore these two steps, any changes made in step 2 will trigger a rearrangement.

Here are three ways to get DOM elements out of the document flow:

  • Hidden elements

  • Use document fragments to build a subtree outside the current DOM and copy it back into the document

  • Copy the original element to a node out of the document, modify the copy, and replace the original element when done

Take the animation element out of the document flow

In general, rearrangement affects only a small part of the render tree, but it can affect a large part, or even the entire render tree. The fewer rearrangements required by the browser, the faster the application will respond. Imagine a situation where there is an animation at the bottom of the page that slides over the rest of the page. This would be an expensive and massive rearrangement! Users are also bound to feel a sense of page-to-page congestion. Therefore, most of the reordering of pages can be avoided using the following steps:

  1. Use absolute positioning to keep animated elements on the page out of the document flow

  2. Animation display stage

  3. At the end of the animation, the element is restored to its position.

IE的:hover

Starting with IE7, IE allows the :hover CSS selector to be used on any element. However, if you have a large number of elements using :hover, you will find that the thief is slow!

Event Delegation

This optimization method is also in front of the job interview in the frequency of the topic. When a page has a large number of elements that need to be bound to an event handler. Each binding event handler comes at a cost, either in page load or run-time execution time. Also, event binding takes up processing time, and the browser needs to keep track of each event handler, which also takes up more memory. It is also the case that when the work is done, most of these event handlers are no longer needed (not 100% of the buttons or links are clicked by the user), so much of the work is unnecessary. The principle of event delegation is simple – events bubble layer by layer and can be captured by the parent element. With event delegation, you only need to bind a handler to the outer element to handle all events that fire on its children. The following points need to be noted:

  • Access the event object to determine the event source

  • Remove bubbling in the document tree as needed

  • Block default actions as needed

summary

To access and manipulate the DOM, you need to cross the bridge that connects the islands of ECMAScript and the DOM. In order to minimize the “toll”, there are a few things to note:

  • Minimize DOM access times

  • Local variables are used to store references to DOM nodes that need to be accessed multiple times

  • If you want to manipulate a collection of HTML elements, it is recommended to copy it into an array

  • Use faster apis: querySelectorAll, for example

  • Pay attention to the number of rearrangements and redraws

  • Event delegation


Share the front end and some fun stuff