为何说DOM操纵很慢

也能够在这里看:http://leozdgao.me/why-dom-slow/

一直都据说DOM很慢,要只管少的去操纵DOM,因而就想进一步去探讨下为什么人人都邑如许说,在网上进修了一些材料,这边整理出来。

起首,DOM对象自身也是一个js对象,所以严格来说,并不是操纵这个对象慢,而是说操纵了这个对象后,会触发一些浏览器行动,比方规划(layout)和绘制(paint)。下面主要先引见下这些浏览器行动,论述一个页面是怎样终究被显现出来的,别的还会从代码的角度,来申明一些不好的实践以及一些优化计划。

浏览器是怎样显现一张页面的

一个浏览器有许多模块,个中担任显现页面的是衬着引擎模块,比较熟习的有WebKit和Gecko等,这里也只会触及这个模块的内容。

先用笔墨大抵论述下这个历程:

  • 剖析HTML,并天生一棵DOM tree

  • 剖析种种款式并连系DOM tree天生一棵Render tree

  • 对Render tree的各个节点盘算规划信息,比方box的位置与尺寸

  • 依据Render tree并应用浏览器的UI层举行绘制

个中DOM tree和Render tree上的节点并不是一一对应,比方一个display:none的节点就在会存在与DOM tree上,而不会出现在Render tree上,因为这个节点不须要被绘制。

《为何说DOM操纵很慢》

上图是Webkit的基础流程,在术语上和Gecko能够会有差别,这里贴上Gecko的流程图,不过文章下面的内容都邑一致运用Webkit的术语。

《为何说DOM操纵很慢》

影响页面显现的要素有许多,比方link的位置会影响首屏显现等。但这里主要集合议论与layout相干的内容。

paint是一个耗时的历程,然则layout是一个更耗时的历程,我们没法肯定layout肯定是自上而下或是自下而上举行的,以至一次layout会牵涉到全部文档规划的从新盘算。

然则layout是肯定没法防止的,所以我们重假如要最小化layout的次数。

什么状况下浏览器会举行layout

在斟酌怎样最小化layout次数之前,要先相识什么时刻浏览器会举行layout。

layout(reflow)平常被称为规划,这个操纵是用来盘算文档中元素的位置和大小,是衬着前主要的一步。在HTML第一次被加载的时刻,会有一次layout以外,js剧本的实行和款式的转变同样会致使浏览器实行layout,这也是本文的主要要议论的内容。

平常状况下,浏览器的layout是lazy的,也就是说:在js剧本实行时,是不会去更新DOM的,任何对DOM的修正都邑被暂存在一个行列中,在当前js的实行上下文完成实行后,会依据这个行列中的修正,举行一次layout。

然则偶然愿望在js代码中马上猎取最新的DOM节点信息,浏览器就不得不提早实行layout,这是致使DOM机能题目的主因。

以下的操纵会打破常规,并触发浏览器实行layout:

  • 经由过程js猎取须要盘算的DOM属性

  • 增加或删除DOM元素

  • resize浏览器窗口大小

  • 转变字体

  • css伪类的激活,比方:hover

  • 经由过程js修正DOM元素款式且该款式触及到尺寸的转变

我们来经由过程一个例子直观的觉得下:

// 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,这个属性是须要盘算获得的,因而就会触发浏览器的一次layout。我们来应用chrome(v47.0)的开发者东西看下(截图中的timeline record已经由挑选,仅显现layout):

《为何说DOM操纵很慢》

上面的例子中,代码起首修正了一个元素的款式,接下来读取另一个元素的clientHeight属性,因为之前的修正致使当前DOM被标记为脏,为了保证能正确的猎取这个属性,浏览器会举行一次layout(我们发明chrome的开发者东西良知的提醒了我们这个机能题目)。

优化这段代码很简单,预先读取所须要的属性,在一起修正即可。

// 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';

看下此次的状况:

《为何说DOM操纵很慢》

下面再引见一些其他的优化计划。

最小化layout的计划

上面提到的一个批量读写是一个,重假如因为猎取一个须要盘算的属性值致使的,那末哪些值是须要盘算的呢?

这个链接里有引见大部份须要盘算的属性:http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html

再来看看别的状况:

面临一系列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';
});

《为何说DOM操纵很慢》

能够很清晰的观察到Animation Frame触发的机遇,MDN上说是在paint之前触发,不过我预计是在js剧本交出掌握权给浏览器举行DOM的invalidated check之前实行。

其他注重点

除了因为触发了layout而致使机能题目外,这边再列出一些其他细节:

缓存挑选器的效果,削减DOM查询。这里要特别体下HTMLCollection。HTMLCollection是经由过程document.getElementByTagName获得的对象范例,和数组范例很相似然则每次猎取这个对象的一个属性,都相当于举行一次DOM查询

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

比方上面的这段代码会致使无穷轮回,所以处置惩罚HTMLCollection对象的时刻要最些缓存。

别的削减DOM元素的嵌套深度并优化css,去除无用的款式对削减layout的盘算量有肯定协助。

在DOM查询时,querySelectorquerySelectorAll应该是末了的挑选,它们功用最壮大,但实行效力很差,假如能够的话,只管用其他要领替换。

下面两个jsperf的链接,能够对比下机能。

https://jsperf.com/getelementsbyclassname-vs-queryselectorall/162
http://jsperf.com/getelementbyid-vs-queryselector/218

本身对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的代码。愿望本身一两年后会过来再看这个题目的时刻,能够有些新的看法。

参考

    原文作者:leozdgao
    原文地址: https://segmentfault.com/a/1190000004114594
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞