Heim  >  Artikel  >  Web-Frontend  >  Warum ist der DOM-Betrieb so langsam?

Warum ist der DOM-Betrieb so langsam?

黄舟
黄舟Original
2017-02-24 13:24:541523Durchsuche

Ich habe immer gehört, dass DOM sehr langsam ist und dass man DOM so wenig wie möglich bedienen sollte, deshalb wollte ich weiter untersuchen, warum das jeder sagte. Ich habe einige Informationen online erfahren und sie hier zusammengestellt.

Erstens ist das DOM-Objekt selbst auch ein JS-Objekt. Genau genommen ist die Bedienung dieses Objekts also nicht langsam, sondern nach der Bedienung dieses Objekts werden einige Browserverhalten ausgelöst , wie etwa Layout (Layout) und Zeichnen (Farbe). Im Folgenden werden hauptsächlich diese Browserverhalten vorgestellt und erläutert, wie eine Seite letztendlich dargestellt wird. Darüber hinaus werden einige schlechte Praktiken und einige Optimierungslösungen aus Code-Perspektive erläutert.

Wie ein Browser eine Seite rendert

Ein Browser verfügt über viele Module, darunter das Rendering-Engine-Modul, das für das Rendern der Seite verantwortlich ist. Zu den bekannteren gehören hier WebKit und Gecko usw Wir werden nur den Inhalt dieses Moduls behandeln.

Beschreiben wir diesen Prozess kurz in Worten:

  • HTML analysieren und einen DOM-Baum generieren

  • Parst verschiedene Stile und kombiniert sie mit dem DOM-Baum, um einen Render-Baum zu generieren

  • Layoutinformationen für jeden Knoten des Render-Baums berechnen, z. B. die Position von die Box und die Größe

  • wird entsprechend dem Render-Baum und unter Verwendung der UI-Ebene des Browsers gezeichnet

Die Knoten im DOM-Baum und im Render Bäume sind keine Eins-zu-Eins-Entsprechungen. Beispielsweise ist ein Knoten mit display:none im DOM-Baum vorhanden, wird jedoch nicht im Render-Baum angezeigt, da dieser Knoten nicht gezeichnet werden muss.

Das obige Bild zeigt den grundlegenden Prozess von Webkit. Die Terminologie unterscheidet sich möglicherweise von der von Gecko, der folgende Inhalt des Artikels wird jedoch verwendet Webkit einheitlich.

Es gibt viele Faktoren, die das Rendern der Seite beeinflussen, wie zum Beispiel die Position des Links, die sich auf das Rendern des ersten Bildschirms auswirkt. Hier konzentrieren wir uns jedoch hauptsächlich auf Layout-bezogene Inhalte.

Das Malen ist ein zeitaufwändiger Prozess, aber das Layout ist ein noch zeitaufwändigerer Prozess. Wir können nicht sicher sein, ob das Layout von oben nach unten oder von unten nach oben erfolgen muss Dokumentlayout.

Aber Layout ist definitiv unvermeidbar , deshalb minimieren wir hauptsächlich die Anzahl der Layouts.

Unter welchen Umständen führt der Browser das Layout durch?

Bevor Sie überlegen, wie Sie die Anzahl der Layouts minimieren können, müssen Sie zunächst verstehen, wann der Browser das Layout durchführt.

Layout (Reflow) wird im Allgemeinen als Layout bezeichnet. Dieser Vorgang wird zum Berechnen der Position und Größe von Elementen im Dokument verwendet und ist ein wichtiger Schritt vor dem Rendern. Wenn HTML zum ersten Mal geladen wird, führt die Ausführung von JS-Skripten und Stiländerungen auch dazu, dass der Browser das Layout ausführt. Dies ist auch der Hauptinhalt, der in diesem Artikel behandelt wird.

Unter normalen Umständen ist das Layout des Browsers faul, das heißt: Wenn das JS-Skript ausgeführt wird, wird das DOM nicht aktualisiert und alle Änderungen am DOM werden vorübergehend in einer Warteschlange gespeichert Nachdem der aktuelle js-Ausführungskontext die Ausführung abgeschlossen hat, wird ein Layout basierend auf den Änderungen in dieser Warteschlange ausgeführt.

Wenn Sie jedoch manchmal die neuesten DOM-Knoteninformationen sofort im JS-Code erhalten möchten, muss der Browser das Layout im Voraus ausführen, was die Hauptursache für DOM-Leistungsprobleme ist.

Die folgenden Vorgänge verstoßen gegen die Regeln und veranlassen den Browser, das Layout auszuführen:

  • Rufen Sie die DOM-Attribute ab, die über js berechnet werden müssen

  • DOM-Elemente hinzufügen oder entfernen

  • Größe des Browserfensters ändern

  • Schriftart ändern

  • Aktivierung von CSS-Pseudoklassen, wie zum Beispiel: Hover

  • Ändern Sie den DOM-Elementstil durch js und der Stil beinhaltet Größenänderungen

Lassen Sie uns ein Beispiel für intuitives Gefühl durchgehen:

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

Hierbei handelt es sich um eine Eigenschaft clientHeight, die berechnet werden muss, damit ein Layout des Browsers ausgelöst wird. Schauen wir uns die Entwicklertools von Chrome (v47.0) an (der Timeline-Datensatz im Screenshot wurde gefiltert und zeigt nur das Layout an):

Im Im obigen Beispiel ändert der Code zuerst den Stil eines Elements und liest dann das clientHeight-Attribut eines anderen Elements. Aufgrund der vorherigen Änderung wird das aktuelle DOM als fehlerhaft markiert Der Browser führt ein Layout aus (wir haben festgestellt, dass die Entwicklertools von Chrome uns gewissenhaft an dieses Leistungsproblem erinnert haben).

Die Optimierung dieses Codes ist sehr einfach. Lesen Sie einfach die erforderlichen Attribute im Voraus und ändern Sie sie gemeinsam.

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

Sehen Sie sich dieses Mal die Situation an:

Lassen Sie uns einige andere Optimierungslösungen vorstellen.

 最小化layout的方案

  上面提到的一个批量读写是一个,主要是因为获取一个需要计算的属性值导致的,那么哪些值是需要计算的呢?

  这个链接里有介绍大部分需要计算的属性:http://www.php.cn/

  再来看看别的情况:

  面对一系列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) + &#39;px&#39;;
});

// Read
var h2 = element2.clientHeight;

// Write
requestAnimationFrame(function() {  
  element2.style.height = (h2 * 2) + &#39;px&#39;;
});

  可以很清楚的观察到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查询时,querySelector和querySelectorAll应该是最后的选择,它们功能最强大,但执行效率很差,如果可以的话,尽量用其他方法替代。

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

  http://www.php.cn/
  http://www.php.cn/

 自己对View层的想法

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

fastdom.read(function() {  
  console.log(&#39;read&#39;);
});

fastdom.write(function() {  
  console.log(&#39;write&#39;);
});

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

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

 

 以上就是为什么说DOM操作很慢的内容,更多相关内容请关注PHP中文网(www.php.cn)!


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn