这几天分享一下我看《高性能 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 的次数。大致方案如下:
- 将需要修改的元素节点脱离文档流。
- 修改脱离文档流的元素节点。
- 将元素带入文档流。
具体的方案有三种:
- 将需要修改的 DOM 隐藏(display: none)后对节点进行修改,最后再将隐藏的节点显示出来。
var ul = document.getElementById('list')
ul.style.display = 'none'
appendDataToElement(ul, data)
ul.style.display = 'block'
- 使用 document.createDocumentFragment() 方法创建文档片段,在文档片段中定义修改的 DOM,最后将文档片段应用到 DOM 中。
var fragment = document.createDocumentFragment()
appendDataToElement(fragment, data)
document.getElementById('list').appendChild(fragment)
- 使用 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 的性能如何(想必是更高的)?
这个问题之后有空我会做个调研写篇文章分享一下~