高性能 JavaScript - DOM

这几天分享一下我看《高性能 JavaScript》的学习笔记,希望能对大家有所帮助。

如果说前面两章日常工作中不太关注,那么 DOM 优化确实我们必知必会的知识点了。

天生就慢

首先,DOM 天生就是非常慢的。为什么呢?因为 DOM 和 JavaScript 运行环境是两个环境,所以两者通信只能通过接口连接。就像是 DOM 和 JavaScript 运行时是两个岛屿,中间只能通过一艘小船来往。

所以,DOM 优化的核心就是尽量将更多的处理停留在 JavaScript 运行时这座小岛上,减少使用小船来往的次数。或者说尽量在 JavaScript 端处理逻辑,只在必要时访问 DOM。

DOM 的修改

  • 由于 API 交互特性,访问 DOM 的次数越多,代码运行的速度必然越慢。
  • 修改 DOM 元素有两种方式:element.innerHTML 和 document.createElement() 。两者的性能在新版本浏览器上差不多,而在老版本浏览器中 innerHTML 会更好。(个人觉得 innerHTML 写法也更加清晰)
  • 另外一种修改 DOM 的方式是通过 element.cloneNodes() 来克隆元素节点,修改克隆的元素然后替换 DOM 中的元素。

HTML 集合

我们使用一些 API 能够获取 HTML 集合,HTML 集合是一个带有 length 属性酷似数组的对象。它有一个很重要的特性就是 HTML 集合会随着 DOM 元素的变化而变化

  • HTML 集合的 length 是会随着 DOM 元素数量变化的,所以不要直接使用 length 属性。
  • 遍历数组要比遍历 HTML 集合的速度快。
  • 如果要使用 HTML 集合的 length,可以先将 length 保存为局部变量。
  • 多使用局部变量保存和引用 HTML 集合的信息,减少访问 DOM 的次数。

以下 DOM API 能够获取 HTML 集合。

  • document.getElementByName()
  • document.getElementByTagName()
  • document.getElementByClass()
  • element.childNodes 是通过某个元素获取他的子元素的,获取的也是 HTML 集合。

更快的 API

对于获取元素节点的 DOM 方法,有只获取元素节点和获取所有节点两类。性能上显然前者的性能会更高。

只获取元素节点 获取所有节点
children childNodes
childElementCount childNodes.length
firstElementChild firstChild
lastElementChild lastChild
nextElementSibling nextSibling
previousElementSibling previousSibling

在遍历 DOM 节点上,很多 DOM 方法可以做到。

  • document.getElementByName()
  • document.getElementByTagName()
  • document.getElementByClass()
  • document.querySelectorAll()
  • ……

其中 document.querySelectorAll() 使用了 CSS 选择器来获取 NodeList 数组对象。这种写法不仅方便,而且性能上也优于其他方法。

所以,如果浏览器支持,尽量使用上述的这些方法,因为这些 API 更快!

DOM 绘制过程

对于 DOM 性能而言,重绘和重排是最重要的知识点了。下面先复习一下浏览器工作原理:

  • 浏览器解析 HTML 获取 DOM 树。
  • 浏览器解析 CSS 获取 CSSOM 规则树。
  • 将 CSS 规则树应用到 DOM 树上构成渲染树。
  • 使用渲染树上的样式计算尺寸和位置,解析排版。
  • 更具渲染树属性生成位图,即绘制。
  • 最后使用浏览器 API 呈现这些有排版的位图。
  • 如果页面元素尺寸有变化,进行重新排版和绘制。
  • 如果页面无尺寸变化,而是像背景颜色这样的变化,则会进行重新绘制。

重排何时发生?

  • 添加或删除可见的 DOM 元素
  • 元素位置改变
  • 元素尺寸改变(包括:margin、padding、border-width、width、height 等属性)
  • 内容改变,如:文本改变、图片尺寸改变。
  • 页面渲染器初始化
  • 浏览器创建尺寸改变

立即重排

其实,在浏览器中重排是有优化机制的。浏览器会队列化修改并批量执行重排行为。但是使用了一些 API 后会立即进行重排行为。主要是一些查询当前布局信息的 API 方法:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle() (IE 中的当前样式)

优化方案是不在布局信息改变时查询布局信息

最小化重绘和重排

最小化重绘和重排的方式是尽量减少修改 DOM 的次数。

// bad
var el = document.getElementById('mydiv')
el.style.borderLeft = '1px'
el.syle.borderRight = '1px'
el.style.padding = '5px'

// good
el.style.cssText = 'border-left: 1px; border-right: 1px; padding: 5px;'

// good
el.className = 'active'

批量修改 DOM 方案很好的减少了修改 DOM 的次数。大致方案如下:

  1. 将需要修改的元素节点脱离文档流。
  2. 修改脱离文档流的元素节点。
  3. 将元素带入文档流。

具体的方案有三种:

  1. 将需要修改的 DOM 隐藏(display: none)后对节点进行修改,最后再将隐藏的节点显示出来。
var ul = document.getElementById('list')
ul.style.display = 'none'
appendDataToElement(ul, data)
ul.style.display = 'block'
  1. 使用 document.createDocumentFragment() 方法创建文档片段,在文档片段中定义修改的 DOM,最后将文档片段应用到 DOM 中。
var fragment = document.createDocumentFragment()
appendDataToElement(fragment, data)
document.getElementById('list').appendChild(fragment)
  1. 使用 document.cloneNode() 克隆元素节点,修改后用克隆节点替换原有节点。
var old = document.getElementById('list')
var clone = old.cloneNode(true)
appendDataToElement(clone, data)
old.parentNode.replaceChild(clone, old)

在基于现有元素尺寸修改尺寸时,最好使用局部变量缓存布局信息,这样可以减少访问 DOM 的次数。

事件委托

事件监听绑定也有一定的性能开销,可以使用事件委托方法来减少嵌套组件的重复事件绑定,具体可以看下 JavaScript 事件委托一文。

小结

DOM 非常慢,非常消耗性能。所以 DOM 性能优化是前端优化非常重要的一环。下面是主要内容:

  • 最小化 DOM 的访问次数,尽量将工作交给 JavaScript 去完成。
  • 小心 HTML 集合与数组的差别 —— HTML 集合会根据 DOM 的改变而发生改变。数组的性能优于 HTML 集合。
  • 使用批量修改的方式减少重排和重绘次数。
  • 使用事件委托减少事件绑定数量。

最后

由于书中内容太多,讲的比较笼统。不过还是希望能够对大家有所帮助吧。如果有什么问题欢迎留言和我沟通。

最后我有个疑问,既然说改变布局会产生重排,那么像 transform + translate 这种变形动画改变了大小的情况,重排的频率如何,是否特别消耗性能。相比于不断的修改 top、left、width 和 height 的性能如何(想必是更高的)?

这个问题之后有空我会做个调研写篇文章分享一下~

你可能感兴趣的:(高性能 JavaScript - DOM)