Maison  >  Article  >  interface Web  >  Pourquoi le fonctionnement du DOM est-il si lent ?

Pourquoi le fonctionnement du DOM est-il si lent ?

黄舟
黄舟original
2017-02-24 13:24:541529parcourir

J'ai toujours entendu dire que DOM est très lent et que vous devriez utiliser DOM le moins possible, j'ai donc voulu explorer davantage pourquoi tout le monde disait cela. J'ai appris quelques informations en ligne et je les ai compilées ici.

Tout d'abord, l'objet DOM lui-même est aussi un objet js, donc à proprement parler, ce n'est pas que l'exploitation de cet objet soit lente, mais qu'après avoir exploité cet objet, certains comportements du navigateur seront déclenchés , tels que la mise en page ( mise en page) et le dessin (peinture). Ce qui suit présente principalement ces comportements du navigateur et explique comment une page est finalement présentée. En outre, il explique également certaines mauvaises pratiques et certaines solutions d'optimisation du point de vue du code.

Comment un navigateur rend une page

Un navigateur possède de nombreux modules, parmi lesquels le module moteur de rendu est responsable du rendu de la page. Les plus familiers incluent WebKit et Gecko, etc., ici. nous seulement Le contenu de ce module sera couvert.

Décrivons brièvement ce processus avec des mots :

  • Analysez le HTML et générez un arbre DOM

  • Analyse différents styles et les combine avec l'arborescence DOM pour générer un Arbre de rendu

  • Calculer les informations de disposition pour chaque nœud de l'arbre de rendu, telles que la position de la boîte et la taille

  • sont dessinées selon l'arbre de rendu et en utilisant la couche UI du navigateur

Les nœuds sur l'arbre DOM et le rendu tree ne sont pas des correspondances biunivoques, par exemple, un nœud avec display:none existera dans l'arborescence DOM, mais n'apparaîtra pas dans l'arborescence de rendu, car ce nœud n'a pas besoin d'être dessiné.

L'image ci-dessus est le processus de base de Webkit. La terminologie peut être différente de celle de Gecko. Voici l'organigramme de Gecko, mais le contenu suivant de l'article sera utilisé. Webkit uniformément.

Il existe de nombreux facteurs qui affectent le rendu de la page, comme la position du lien affectera le rendu du premier écran, etc. Mais ici, nous nous concentrons principalement sur le contenu lié à la mise en page.

La peinture est un processus qui prend du temps, mais la mise en page est un processus encore plus long. Nous ne pouvons pas savoir si la mise en page doit être descendante ou ascendante. Même une seule mise en page impliquera le recalcul complet de. mise en page des documents.

Mais la mise en page est définitivement inévitable , c'est pourquoi nous minimisons principalement le nombre de mises en page.

Dans quelles circonstances le navigateur effectuera-t-il la mise en page

Avant d'envisager comment minimiser le nombre de mises en page, vous devez d'abord comprendre quand le navigateur effectuera la mise en page.

La mise en page (reflow) est généralement appelée mise en page . Cette opération permet de calculer la position et la taille des éléments dans le document et constitue une étape importante avant le rendu. Lorsque HTML est chargé pour la première fois, en plus d'une mise en page, l'exécution de scripts js et les modifications de styles entraîneront également l'exécution de la mise en page par le navigateur. C'est également le contenu principal qui sera abordé dans cet article.

Dans des circonstances normales, la mise en page du navigateur est paresseuse, c'est-à-dire : lorsque le script js est exécuté, le DOM ne sera pas mis à jour, et toute modification du DOM sera temporairement stockée dans une file d'attente . Une fois l'exécution du contexte d'exécution js actuel terminée, une mise en page sera effectuée en fonction des modifications apportées à cette file d'attente.

Cependant, parfois, si vous souhaitez obtenir immédiatement les dernières informations sur les nœuds DOM dans le code js, le navigateur doit exécuter la mise en page à l'avance, ce qui est la principale cause des problèmes de performances du DOM.

Les opérations suivantes enfreindront les règles et déclencheront l'exécution de la mise en page par le navigateur :

  • Obtenir les attributs DOM qui doivent être calculés via js

  • Ajouter ou supprimer des éléments DOM

  • Redimensionner la taille de la fenêtre du navigateur

  • Changer la police

  • Activation des pseudo-classes CSS, telles que : hover

  • Modifier le style de l'élément DOM via js et le style implique des changements de taille

Passons Un exemple de ressenti intuitif :

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

Cela implique une propriété clientHeight, qui doit être calculée, elle déclenchera donc une mise en page du navigateur. Utilisons les outils de développement de Chrome (v47.0) pour y jeter un œil (l'enregistrement de la chronologie dans la capture d'écran a été filtré et affiche uniquement la mise en page) :

Dans le Dans l'exemple ci-dessus, le code modifie d'abord le style d'un élément, puis lit l'attribut clientHeight d'un autre élément. En raison de la modification précédente, le DOM actuel est marqué comme sale. Afin de garantir que cet attribut peut être obtenu avec précision, le Le navigateur effectuera une mise en page (nous avons constaté que les outils de développement de Chrome nous rappelaient consciencieusement ce problème de performances).

L'optimisation de ce code est très simple, il suffit de lire les attributs requis à l'avance et de les modifier ensemble.

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

Regardez la situation cette fois :

Introduisons quelques autres solutions d'optimisation.

 最小化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)!


Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn