也能够在这里看:http://leozdgao.me/why-dom-slow/javascript
一直都据说DOM很慢,要尽可能少的去操做DOM,因而就想进一步去探究下为何你们都会这样说,在网上学习了一些资料,这边整理出来。css
首先,DOM对象自己也是一个js对象,因此严格来讲,并非操做这个对象慢,而是说操做了这个对象后,会触发一些浏览器行为,好比布局(layout)和绘制(paint)。下面主要先介绍下这些浏览器行为,阐述一个页面是怎么最终被呈现出来的,另外还会从代码的角度,来讲明一些很差的实践以及一些优化方案。html
一个浏览器有许多模块,其中负责呈现页面的是渲染引擎模块,比较熟悉的有WebKit和Gecko等,这里也只会涉及这个模块的内容。html5
先用文字大体阐述下这个过程:java
解析HTML,并生成一棵DOM treeweb
解析各类样式并结合DOM tree生成一棵Render treechrome
对Render tree的各个节点计算布局信息,好比box的位置与尺寸编程
根据Render tree并利用浏览器的UI层进行绘制数组
其中DOM tree和Render tree上的节点并不是一一对应,好比一个display:none
的节点就在会存在与DOM tree上,而不会出如今Render tree上,由于这个节点不须要被绘制。浏览器
上图是Webkit的基本流程,在术语上和Gecko可能会有不一样,这里贴上Gecko的流程图,不过文章下面的内容都会统一使用Webkit的术语。
影响页面呈现的因素有许多,好比link的位置会影响首屏呈现等。但这里主要集中讨论与layout相关的内容。
paint是一个耗时的过程,然而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):
上面的例子中,代码首先修改了一个元素的样式,接下来读取另外一个元素的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';
看下此次的状况:
下面再介绍一些其余的优化方案。
上面提到的一个批量读写是一个,主要是由于获取一个须要计算的属性值致使的,那么哪些值是须要计算的呢?
这个连接里有介绍大部分须要计算的属性:http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html
再来看看别的状况:
针对一系列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
的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 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查询时,querySelector
和querySelectorAll
应该是最后的选择,它们功能最强大,但执行效率不好,若是能够的话,尽可能用其余方法替代。
下面两个jsperf的连接,能够对比下性能。
https://jsperf.com/getelementsbyclassname-vs-queryselectorall/162
http://jsperf.com/getelementbyid-vs-queryselector/218
上面的内容理论方面的东西偏多,从实践的角度来看,上面讨论的内容,正好是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的代码。但愿本身一两年后会过来再看这个问题的时候,能够有些新的看法。