Maison  >  Article  >  interface Web  >  Introduction détaillée aux raisons pour lesquelles les opérations DOM en JavaScript sont lentes

Introduction détaillée aux raisons pour lesquelles les opérations DOM en JavaScript sont lentes

黄舟
黄舟original
2017-03-08 14:59:381216parcourir

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 plus en détail 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, comme mise en page et 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écrivez d'abord brièvement ce processus dans le texte :

  • Analysez le HTML et générez une arborescence DOM

  • Analysez chaque style et combinez-le 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 et la taille de la boîte

  • Dessinez en fonction de l'arborescence de rendu et utilisez la couche d'interface utilisateur du navigateur

Les nœuds de l'arborescence DOM et de l'arborescence de rendu ne sont pas une correspondance individuelle. Par exemple, un nœud "display:none"" n'existera que sur l'arborescence DOM et n'apparaîtra pas sur l'arborescence de rendu, car ce nœud n'a pas besoin d'être dessiné

Ce qui précède. La figure est le processus de base de Webkit, en termes de Gecko peut être différent. Voici l'organigramme de Gecko, mais le contenu suivant de l'article utilisera uniformément la terminologie de Webkit

<.>Il existe de nombreux facteurs qui affectent le rendu de la page, tels que 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 prend beaucoup de temps. processus, mais la mise en page est un processus plus long. Nous ne pouvons pas déterminer la mise en page. Elle se fait de haut en bas ou de bas en haut, et même une seule mise en page impliquera le recalcul de la mise en page entière du document

Mais la mise en page. est définitivement inévitable, nous voulons donc principalement minimiser le nombre de mises en page

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

Avant de réfléchir à la manière de minimiser le nombre de mises en page, vous devez d'abord comprendre. lorsque le navigateur effectuera la mise en page (reflow) est généralement appelé mise en page. Cette opération est utilisée pour calculer la position et la taille des éléments dans le document. C'est une étape importante avant le rendu. Lorsque le HTML est chargé pour la première fois, il y aura. une exécution de script js en plus de la mise en page entraînera également l'exécution par le navigateur de la mise en page, ce qui est également le contenu principal de cet article

Généralement, 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. Toutes les modifications apportées au DOM seront temporairement stockées dans une file d'attente une fois l'exécution du contexte js actuel terminée, une mise en page sera effectuée en fonction des modifications de 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. C'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 :

Obtenez 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 du CSS pseudo-classes, telles que : hover

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

  • Ressentons-le intuitivement un exemple :

clientHeight, cet attribut doit être calculé, il 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) :

// Read
var h1 = element1.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + &#39;px&#39;;

// Read (triggers layout)
var h2 = element2.clientHeight;

// Write (invalidates layout)
element2.style.height = (h2 * 2) + &#39;px&#39;;

// Read (triggers layout)
var h3 = element3.clientHeight;

// Write (invalidates layout)
element3.style.height = (h3 * 2) + &#39;px&#39;;

Dans le Dans l'exemple ci-dessus, le code modifie d'abord le style d'un élément, puis lit l'attribut

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 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.

clientHeight

Regardez la situation cette fois-ci :

// Read
var h1 = element1.clientHeight;  
var h2 = element2.clientHeight;  
var h3 = element3.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + &#39;px&#39;;  
element2.style.height = (h2 * 2) + &#39;px&#39;;  
element3.style.height = (h3 * 2) + &#39;px&#39;;

Voici d'autres plans d'optimisation.

Une solution pour minimiser la mise en page

La lecture et l'écriture par lots mentionnées ci-dessus sont principalement causées par l'obtention d'une valeur d'attribut qui doit être calculée. Alors, quelles valeurs doivent être calculées ?

这个链接里有介绍大部分需要计算的属性: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查询时,querySelectorquerySelectorAll应该是最后的选择,它们功能最强大,但执行效率很差,如果可以的话,尽量用其他方法替代。

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

1)http://www.php.cn/

2)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的代码。希望自己一两年后会过来再看这个问题的时候,可以有些新的见解。


Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

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