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 node | Return Node |
---|---|
children | childNodes |
childElementCount | childNodes.length |
firstElementChild | firstChild |
lastElementChild | lastChild |
nextElementSibling | nextSibling |
previousElementSibling | previousSibling |
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:
- Detach elements from document flow
- Modify the operation
- 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:
- Hide the element, then modify the element, and finally display it
- Working with document fragments
- 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:
- Separating elements from the document flow by absolute positioning
- Apply animation effects to elements
- 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:
- Capture (root element - > target element)
- Trigger on target element
- 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