I have always heard that DOM is very slow, so I want to operate DOM as little as possible, so I want to further explore why people say so. I have learned some materials on the Internet and sorted them out here.
First, the DOM object itself is a JS object, so it’s not that manipulating it is slow, strictly speaking, but that manipulating it triggers browser actions like layout and paint. I’ll take a look at these browser behaviors, explain how a page ends up being rendered, and explain some bad practices and optimizations from a code standpoint.
How does a browser render a page
There are many modules in a browser, among which the rendering engine module is responsible for rendering the page, which is more familiar with WebKit and Gecko, etc., and only this module will be covered here. Here’s a quick rundown of the process:
- Parse the HTML and generate a DOM tree
- Parse the various styles and combine them with the DOM tree to generate a Render tree
- Compute layout information for each node of Render Tree, such as the position and size of the box
- Draw according to the Render Tree and using the BROWSER’S UI layer
Among themThe DOM tree and the nodes on the Render Tree do not correspond exactlyFor example, a “display: None” node will only exist in the DOM tree, not the Render Tree, because the node does not need to be drawn.
The figure above shows the basic flow of Webkit, which may be different from Gecko in terms of terminology. Gecko’s flow chart is attached here, but the rest of the article will use Webkit terminology.
There are many factors affecting page rendering, such as the location of link will affect the first screen rendering. But I’ll focus on layout-related content here.
Paint is a time-consuming process, while layout is a more time-consuming process. We cannot determine whether layout is top-down or bottom-up, and even a layout will involve the recalculation of the entire document layout.
However, layout is inevitable, so we mainly minimize the number of layout.
When does a browser perform a layout
Before you think about how to minimize layout times, it’s important to know when the browser will do layout.
Layout (reflow) is commonly referred to as layout. This operation is used to calculate the position and size of elements in the document and is an important step before rendering. When the HTML is first loaded, there will be a layout, js script execution and style changes will also cause the browser to execute layout, which is the main topic of this article.
The layout of the browser is lazy, which means that the DOM is not updated during the execution of the JS script. Any changes to the DOM are temporarily stored in a queue, and a layout is performed based on the changes in the queue after the current JS execution context is complete.
However, if you want to get the latest DOM node information in js code immediately, the browser will have to execute layout in advance, which is the main cause of DOM performance problems.
The following action breaks the rules and triggers the browser to execute layout
- Get the DOM properties to be calculated through JS
- Add or remove DOM elements
- Resize Specifies the browser window size
- Change the font
- Activation of CSS pseudo-classes, such as hover
- Modify the DOM element style with JS and the style involves changing the size
Let’s go through an example to get an intuitive feeling:
// Read
var h1 = element1.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
// Read (triggers layout)
var h2 = element2.clientHeight;
// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';
// Read (triggers layout)
var h3 = element3.clientHeight;
// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px'; 1234567891011121314151617Copy the code
The clientHeight property is computed and triggers a layout in the browser. Take a look at the Timeline record in Chrome (V47.0) using the developer tools.
In the above example, the code first changes the style of one element, and then reads the clientHeight attribute of another element. The DOM is marked dirty because of the previous modification. To ensure that this attribute is accurately retrieved, The browser will perform a layout (we found that chrome’s developer tools are warning us about this performance issue).
Tuning this code is simple: read the required properties up front and modify them together.
// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px'; 123456789Copy the code
Take a look at this time:
Here are some other optimizations.
Minimize layout schemes
The above mentioned a batch read and write is a, mainly because of the acquisition of a need to calculate the value of the attribute, so which values need to calculate?
There are introduced in this link are most in need of computing properties: gent.ilcore.com/2011/03/how…
Let’s look at something else:
Faced with a series of DOM operations
– Documentfragment-display: none-clonenode For example (taking the documentFragment as an example) : – DocumentFragment-display: none-clonenode
var fragment = document.createDocumentFragment();
for (var i=0; i < items.length; i++){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i);
fragment.appendChild(item);
}
list.appendChild(fragment); 1234567Copy the code
The core idea of these optimizations is the same: perform a series of operations on a node that is not on the Render Tree, and then add that node back to the Render Tree, so that no matter how complex the DOM operation is, it will only trigger a layout once.
Face style changes
We need to know that not all style changes will trigger a Layout, because we know that layout is responsible for calculating the size of the RenderObject. If I change a color, I will not trigger a Layout.
There is a website CSS Triggers that details the effects of the various CSS properties on the browser’s implementation of Layout and paint.
In the case below, as in the optimization section above, pay attention to the reading and writing.
elem.style.height = "100px"; // mark invalidated
elem.style.width = "100px";
elem.style.marginRight = "10px";
elem.clientHeight // force layout here 12345Copy the code
But let’s mention animation, this is js animation, for example:
function animate (from, to) {
if (from === to) return
requestAnimationFrame(function () {
from += 5
element1.style.height = from + "px"
animate(from, to)
})
}
animate(100, 500) 1234567891011Copy the code
Every frame of an animation will result in a layout, which is unavoidable, but to reduce the performance penalty of the layout caused by animation, you can definitely position the animation elements so that the animation elements are separated from the text stream and the layout calculation is greatly reduced.
Using requestAnimationFrame
Any operations that might result in a redraw should be put into the requestAnimationFrame
In a real-world project, the code is divided into modules, making it difficult to organize batch reads and writes like the above example. Write operations can then be placed in the Callback of the requestAnimationFrame before the next paint.
// Read
var h1 = element1.clientHeight;
// Write
requestAnimationFrame(function() {
element1.style.height = (h1 * 2) + 'px';
});
// Read
var h2 = element2.clientHeight;
// Write
requestAnimationFrame(function() {
element2.style.height = (h2 * 2) + 'px'; }); 123456789101112131415Copy the code
It is clear when the Animation Frame is triggered. MDN says it is triggered before Paint, but I assume it is executed before the JS script cedes control to the browser for the DOM invalidated check.
Other Points for attention
In addition to the performance issues caused by triggering the layout, here are some other details:
Caching the results of the selector, reducing DOM queries. HTMLCollection is a special mention here. HTMLCollection is through the document. The getElementByTagName object types, and array type is very similar but every time to obtain an attribute, this object is the equivalent of a DOM query:
var divs = document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++){ //infinite loop
document.body.appendChild(document.createElement("div")); } 1234Copy the code
For example, the code above causes an infinite loop, so HTMLCollection objects need to be cached.
In addition, it is helpful to reduce the nesting depth of DOM elements and optimize CSS to remove useless styles to reduce layout calculation.
QuerySelector and querySelectorAll should be the last choices when it comes to DOM queries; they are the most powerful, but poorly executed, so use other methods instead if you can.
Here are two links to Jsperf to compare performance.
1) jsperf.com/getelements…
2) jsperf.com/getelementb…
My thoughts on the View layer
The above content is more theoretical, but from a practical point of View, this is exactly what the View layer needs to deal with. There is already a library called FastDOM that does this, but its code looks like this:
fastdom.read(function() {
console.log('read');
});
fastdom.write(function() {
console.log('write'); }); 1234567Copy the code
The problem is obvious, leading to callback hell, and it can be expected that imperative code like FastDOM is not extensible, the key is that with requestAnimationFrame it becomes an asynchronous programming problem. To synchronize read and write states, we need to write a Wrapper around the DOM to control asynchronous reads and writes.
In general, try to avoid the problems mentioned above, but with libraries such as jQuery, the layout problem is the abstraction of the library itself. React introduces its own component model, uses the Virtual DOM to reduce DOM manipulation, and can only have a layout every time state changes. I don’t know if there is any requestAnimationFrame inside. Then prepare to learn the React code. Hopefully I’ll come back in a year or two with some new insights.