《高性能JavaScript》读书笔记(三)

《高性能JavaScript》第三章主要讲的是DOM操作的优化。上两篇的读书笔记自己读过之后感觉更像是对内容的自我消化后的按原文复述,所以这篇调整一下内容的结构,只讲关键点。

DOM操作的优化问题

DOM操作涉及以下三类问题:

  1. 访问和修改DOM元素
  2. 修改DOM元素的样式所导致的重绘(repaint)跟重排(reflow)
  3. 通过DOM事件处理与用户的交互

DOM

DOM,文档对象模型,是独立于语言,用于操作XML跟HTML文档的程序接口。DOM是有规范的,但是实现是由各个浏览器方独自实现支持,因此,我们在开发页面时,需要对不同的浏览器做不同的兼容处理(比如IE,不过现在IE已经很少了)。

DOM是程序接口,我们在开发的时候,会根据需求去实现页面与用户的互动。但因为DOM是独立的,每次需要获取节点都需要各种调用,因此这些调用的增多会影响页面的性能。

DOM访问与修改

其实DOM的访问跟修改可以联系上一章提及的嵌套对象的内容。DOM中的每个节点都是一个对象,如果节点下存在子节点,则这个节点就是一个嵌套对象的例子。其一是DOM是程序接口,相对独立,调用接口都是存在一定的响应时间;其二是DOM中存在大量的嵌套对象,在搜索查询上也是需要一定的处理时间。因此,DOM的访问与修改会存在以下优化点:

  1. DOM的重复调用
  2. DOM集合元素的重复查询
  3. 准确的获得DOM元素集合

DOM的重复调用

简单的说,开发中经常会把后端穿来的数据修改渲染到页面上,书中提到最坏的情况是循环访问或修改元素:

1
2
3
4
5
function innerHTMLLoop () {
for (var count = 0; count < 15000; count++) {
document.getElementById('here').innerHTML += 'a';
}
}

代码的问题在于每次循环中都会读取一次跟重写一次innerHTML,而且循环次数越多,读取跟重写都会不断增加。

所以,优化的方案是,先把需要添加的节点保存在局部变量中,然后在一次性读取跟重写页面结构。
如果出现大量的重复节点,则可以考虑克隆已有的元素,即使用element.cloneNode()替代document.createElement()

DOM元素集合的重复查询

HTML集合是包含DOM节点引用的类数组对象,包括我们常用的document.getElementByName()document.images等类似的方法所得到的元素集合。这些元素集合只是一个类似数组的列表,但同样提供了类似数组的length属性。

首先,在利用document.getElementByName()这类查询方法的时候,因为每次查询都会调用document,所以查询的速度回比较慢。但当因为需要多次获取元素集合的属性时,则会形成一个非常大的性能消耗。

另外,元素集合的length属性不同于数组的length属性,不仅当HTML文档发生更新的时候会自动更新,当调用它的时候也会重复查询,加上集合中的元素都是DOM对象,查询速度更慢,消耗更大。

解决的方案,重点放在减少对document的调用跟文档查询。当遇到需要重复调动document.getElementByName()这类查询方法,可以根据不同的情况,进行不同的优化。比如当需要重复查询同一个DOM节点时,可以考虑用局部变量去复制;当代码中多次需要调动document的方法时,也可以先把document复制到局部变量中(这个做法也有风险,当HTML文档结构发生改变时,局部变量是不会改变的);当需要多次调用length属性时,同样先用局部变量存储下来,再执行其他操作。

准确的获得DOM元素集合

大多数情况下,我们都是需要获得一些准确的DOM元素集合进行操作,但是存在两类问题:有些方法得到的元素集合会比我们需要的要多(比如空的文本节点),这个时候就会在处理集合是需要花时间去过滤筛选的;另外,获取DOM元素集合的调用太复杂。因此,有两类方法可以更准确的获取DOM元素集合:

  1. 使用以下表格(书中给出的)左边的属性,替代右边的属性,可以更准确的获得DOM元素集合:
属性名 被替代的属性
children childNodes
ChildElementCount childNodes.length
firstElementChild firstChild
lastElementChild lastChild
nextElementSibling nextSibling
previousElementSibling previousSibling
  1. 使用querySeletorALL()方法跟querySeletor()方法。这两个方法可以免掉多次使用getElementById()getElementByClass()这类方法,通过CSS选择器一步到位。

重排(reflow)与重绘(repaint)

简单的说,HTML文档跟CSS文件在首次加载时,得到DOM树跟渲染树,然后才会开始绘制。当DOM出现变化时,浏览器就需要重新计算元素的几何属性,并重新构造渲染树,这个过程叫重排(reflow)。浏览器也会重新绘制受影响的部分,这个过程叫重绘(repaint)。但有些情况下只会发生重绘,不需要重排。每次重排跟重绘都会导致页面的反应迟钝,使客户体验变差。所以我们要了解什么情况会触发重排跟重绘。

触发重排的条件

这些情况下会发生重排:

  1. 添加或删除可见的DOM元素
  2. 元素位置改变
  3. 元素尺寸改变(包括:外边距、内边距、边框厚度、宽度、高度等属性改变)
  4. 内容改变,例如:文本改变或图片被另一个不同尺寸的图片替代
  5. 页面渲染器初始化
  6. 浏览器窗口尺寸改变

也有当滚动条出现时触发整个页面的重排。

触发重绘的条件

重绘只发生在当元素的可见的外观被改变,但并没有影响到布局的时候。比如背景颜色,字体颜色。

渲染树渲染队列与刷新

大多数浏览器是通过队列来修改并批量执行重排过程,这样可以优化并降低消耗,但同时也会让程序在不知不觉中就触发了重排。以下获取布局信息的方法会导致列队刷新:

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight
  2. scrollTop, scrollLeft, scrollWidth, scrollHeight
  3. clientTop, clientLeft, clientWidth, clientHeight
  4. getComputedStyle() (currentStyle in IE)

这些方法需要返回最新的布局信息,浏览器就会不得不执行渲染队列中待处理的变化并触发重排以返回正确的值。

最小化重绘与重排

以下的几种方式可以减少重绘与重排的次数:

  1. 改变样式。改变样式时,如果要对元素进行多个样式的修改,可以用element.style.cssText来一次处理,代替掉对每个样式单独的修改;
  2. 修改DOM。修改DOM时,有几个思路可以参考:可以把需要修改的DOM元素脱离文档流,再做修改,最后再放回文档中;可以把需要修改的DOM元素复制出来,修改完毕后直接替换到文档中;
  3. 减少使用会触发渲染队列刷新的方法;
  4. 让元素脱离动画流。当页面有带动画交互效果的元素时,最好在动画开始前把动画的元素做绝对定位,同时把其他元素先移到动画结束时的位置,再执行动画效果。这样可以减少动画过程中这个页面的大规模重排;
  5. 针对IE,减少大规模使用:hover。

通过DOM事件处理与用户的交互

现在很多页面都是存在大量的交互,而这些交互如果都要单独绑定事件处理器,则会应该性能。因此,书中提出建议使用事件代理,只需在外层元素绑定一个处理器,来处理所有子元素上触发的事件。

最后

本章主要是针对DOM做优化,更多的是讨论如何减少DOM的调用修改。每种操作都可以根据不同的需求进行不同的实现以达到优化的目的。

文中用到的代码参考《高性能JavaScript》第三章。