Home  >  Article  >  Web Front-end  >  Detailed introduction to the reasons why DOM operations in JavaScript are slow

Detailed introduction to the reasons why DOM operations in JavaScript are slow

黄舟
黄舟Original
2017-03-08 14:59:381054browse

I have always heard that DOM is very slow and that you should operate DOM as little as possible, so I wanted to further explore why everyone said this. I learned some information online and compiled it here.

First of all, the DOM object itself is also a js object, so strictly speaking, it is not that operating this object is slow, but that after operating this object, some browser behaviors will be triggered, such as layout and Paint. The following mainly introduces these browser behaviors and explains how a page is finally presented. In addition, it also explains some bad practices and some optimization solutions from a code perspective.

How a browser renders a page

A browser has many modules, among which the rendering engine module is responsible for rendering the page. The more familiar ones include WebKit and Gecko, etc., here we only The content of this module will be covered.

Let’s briefly describe this process in words:

  • Parse HTML and generate a DOM tree

  • Parse each A style and combined with the DOM tree to generate a Render tree

  • Calculate layout information for each node of the Render tree, such as the position and size of the box

  • Draw based on the Render tree and use the browser's UI layer

The nodes on the DOM tree and the Render tree are not in one-to-one correspondence, such as a "display:none"# The ## node will only exist on the DOM tree and will not appear on the Render tree, because this node does not need to be drawn.

The above picture is the basic process of Webkit. The terminology may be different from Gecko. Here is the flow chart of Gecko, but the following content of the article will use Webkit uniformly. terms of.

#There are many factors that affect page rendering. For example, the position of the link will affect the first screen rendering. But here we mainly focus on layout-related content.

Paint is a time-consuming process, but layout is an even more time-consuming process. We cannot be sure whether layout must be top-down or bottom-up. Even one layout will involve the entire Recalculation of document layout.

But layout is definitely unavoidable, so our main goal is to minimize the number of layouts.

Under what circumstances will the browser perform layout

Before considering how to minimize the number of layouts, you must first understand when the browser will perform layout.

Layout (reflow) is generally called layout. This operation is used to calculate the position and size of elements in the document. It is an important step before rendering. When HTML is loaded for the first time, in addition to a layout, the execution of js scripts and changes in styles will also cause the browser to execute layout. This is also the main content to be discussed in this article.

Under normal circumstances, the layout of the browser is lazy, that is to say: when the js script is executed, the DOM will not be updated, and any modifications to the DOM will be temporarily stored in a queue. After the current js execution context completes execution, a layout will be performed based on the modifications in this queue.

However, sometimes if you want to get the latest DOM node information immediately in the js code, the browser has to execute the layout in advance, which is the main cause of DOM performance problems.

The following operations will break the rules and trigger the browser to execute layout:

  • Get the DOM attributes that need to be calculated through js

  • Add or remove DOM elements

  • resize browser window size

  • Change font

  • Activation of css pseudo-classes, such as:hover

  • Modify the DOM element style through js and the style involves size changes

Let’s pass An example of 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';

clientHeight. This attribute needs to be calculated, so it will trigger a layout of the browser. Let’s use the developer tools of chrome (v47.0) to take a look (the timeline record in the screenshot has been filtered, and only the layout is displayed):

In the above example, The code first modifies the style of one element, and then reads the

clientHeight attribute of another element. Due to the previous modification, the current DOM is marked as dirty. In order to ensure that this attribute can be accurately obtained, the browser will Perform a layout (we found that Chrome's developer tools conscientiously reminded us of this performance issue).

Optimizing this code is very simple, just read the required attributes in advance 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';

Look at the situation this time:

The following will introduce some other optimization solutions.

The solution to minimize layout

The batch read and write mentioned above is mainly caused by obtaining an attribute value that needs to be calculated. So which values ​​need to be calculated?

这个链接里有介绍大部分需要计算的属性://m.sbmmt.com/

再来看看别的情况:

面对一系列DOM操作

针对一系列DOM操作(DOM元素的增删改),可以有如下方案:

  • documentFragment

  • display: none

  • cloneNode

比如(仅以documentFragment为例):

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);

这类优化方案的核心思想都是相同的,就是先对一个不在Render tree上的节点进行一系列操作,再把这个节点添加回Render tree,这样无论多么复杂的DOM操作,最终都只会触发一次layout。

面对样式的修改

针对样式的改变,我们首先需要知道并不是所有样式的修改都会触发layout,因为我们知道layout的工作是计算RenderObject的尺寸和大小信息,那么我如果只是改变一个颜色,是不会触发layout的。

这里有一个网站CSS triggers,详细列出了各个CSS属性对浏览器执行layout和paint的影响。

像下面这种情况,和上面讲优化的部分是一样的,注意下读写即可。

elem.style.height = "100px"; // mark invalidated  
elem.style.width = "100px";  
elem.style.marginRight = "10px";

elem.clientHeight // force layout here

但是要提一下动画,这边讲的是js动画,比如:

function animate (from, to) {  
  if (from === to) return

  requestAnimationFrame(function () {
    from += 5
    element1.style.height = from + "px"
    animate(from, to)
  })
}

animate(100, 500)

动画的每一帧都会导致layout,这是无法避免的,但是为了减少动画带来的layout的性能损失,可以将动画元素绝对定位,这样动画元素脱离文本流,layout的计算量会减少很多。

使用requestAnimationFrame

任何可能导致重绘的操作都应该放入requestAnimationFrame

在现实项目中,代码按模块划分,很难像上例那样组织批量读写。那么这时可以把写操作放在requestAnimationFrame的callback中,统一让写操作在下一次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';
});

可以很清楚的观察到Animation Frame触发的时机,MDN上说是在paint之前触发,不过我估计是在js脚本交出控制权给浏览器进行DOM的invalidated check之前执行。

其他注意点

除了由于触发了layout而导致性能问题外,这边再列出一些其他细节:

缓存选择器的结果,减少DOM查询。这里要特别提下HTMLCollection。HTMLCollection是通过document.getElementByTagName得到的对象类型,和数组类型很类似但是每次获取这个对象的一个属性,都相当于进行一次DOM查询:

var ps = document.getElementsByTagName("p");  
for (var i = 0; i < ps.length; i++){  //infinite loop  
  document.body.appendChild(document.createElement("p"));
}

比如上面的这段代码会导致无限循环,所以处理HTMLCollection对象的时候要做些缓存。

另外,减少DOM元素的嵌套深度并优化css,去除无用的样式对减少layout的计算量有一定帮助。

在DOM查询时,querySelectorquerySelectorAll应该是最后的选择,它们功能最强大,但执行效率很差,如果可以的话,尽量用其他方法替代。

下面两个jsperf的链接,可以对比下性能。

1)//m.sbmmt.com/

2)//m.sbmmt.com/

自己对View层的想法

上面的内容理论方面的东西偏多,从实践的角度来看,上面讨论的内容,正好是View层需要处理的事情。已经有一个库FastDOM来做这个事情,不过它的代码是这样的:

fastdom.read(function() {  
  console.log('read');
});

fastdom.write(function() {  
  console.log('write');
});

问题很明显,会导致callback hell,并且也可以预见到像FastDOM这样的imperative的代码缺乏扩展性,关键在于用了requestAnimationFrame后就变成了异步编程的问题了。要让读写状态同步,那必然需要在DOM的基础上写个Wrapper来内部控制异步读写,不过都到了这份上,感觉可以考虑直接上React了……

总之,尽量注意避免上面说到的问题,但如果用库,比如jQuery的话,layout的问题出在库本身的抽象上。像React引入自己的组件模型,用过virtual DOM来减少DOM操作,并可以在每次state改变时仅有一次layout,我不知道内部有没有用requestAnimationFrame之类的,感觉要做好一个View层就挺有难度的,之后准备学学React的代码。希望自己一两年后会过来再看这个问题的时候,可以有些新的见解。


The above is the detailed content of Detailed introduction to the reasons why DOM operations in JavaScript are slow. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn