为什么操作 DOM 慢?
浏览器通常要求 DOM 实现和 JavaScript 实现保持相互独立。例如:Google 使用 WebKit 的 WebCore 库渲染页面,但实现了自己的 JavaScript 引擎 V8 。
所以,两个独立的部分以功能接口连接就会带来性能损耗。
因此,你访问 DOM 越多,代码的执行速度也就越慢。
innerHTML 和 createElement 哪个性能更好?
innerHTML 在老式浏览器上比 createElement。
但是在新版本浏览器上就不那么明显了,甚至在新的基于 WebKit 的浏览器上 createElement 更快。
如果在一个性能苛刻的操作中更新一大块 HTML 页面, innerHTML 在大多数浏览器中执行更快。但对于大多数日常操作而言,其差异并不大,所以你应当根据代码可读性,可维护性,团队习惯,代码风格来综合决定采用哪种方法。
重绘 和 重排(Reflow)
当 DOM 改变影响到元素的几何属性(宽和高)—— 例如改变了边框宽度或在段落中添加文字,将发生一系列后续动作 —— 浏览器需要重新计算元素的几何属性,而且其它元素的几何属性和位置也会因此改变收到影响。浏览器使渲染树上受到影响的部分失效,然后重构渲染树。这个过程被称作重排。重排完成时,浏览器在一个重绘进程中重新绘制屏幕上受影响的部分。
不是所有的 DOM 改变都会影响几何属性。例如改变一个元素的颜色不会影响它的宽度或高度。在这种情况下,只需要重绘(不需要重排版),因为元素的布局没有改变。
导致重排的原因有:
- 添加或删除 DOM 元素
- 元素位置、大小、内容改变:文字大小改变
- 最初的页面渲染
- 浏览器窗口大小改变
- 滚动条出现
根据改变的性质,渲染树上或大或小的一部分需要重新计算。某些改变可导致重排整个页面:例如,当一个滚动条出现时。
常见重排元素:
- 大小有关的:width height padding margin border-width border min-height
- 布局有关的:display top positon float left right bottom
- 字体有关的:font-size text-align font-weight font-family line-height white-space vertical-align
- 隐藏有关的:overflow overflow-x overflow-y
常见的重绘元素:
- 颜色:color background
- 边框样式:border-style outline-color outline outline-style border-radius box-shadow outline-width
- 背景有关:background background-image background-position background-repeat background-size
因为重排和重绘的操作十分昂贵,浏览器会通过队列化修改并批量执行的方式,来进行优化。然而,有些操作会导致强制刷新队列动作:
- offsetTop,offsetLeft,offsetWidth,offsetHeight
- scrollTop,scrollLeft,scrollWidth,scrollHeight
- clientTop,clientLeft,clientWidth,clientHeight
- getComputedStyle()(currentStyle in IE)(在 IE 中此函数称为 currentStyle)
布局信息由这些属性和方法返回最新的数据,所以浏览器不得不运行渲染队列中待改变的项目并重新排版以返回正确的值。
优化
- 将多个 DOM 和风格改变合并到一个批次中一次性执行
例子:
var el = document.getElementById('mydiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
上面的代码改变了三个风格属性,每次改变都影响到元素的几何属性。在这个糟糕的例子中,它导致浏览器重排版了三次。大多数现代浏览器优化了这种情况只进行了一次排版,但是在老式浏览器中,效率将十分低下。而且,此代码访问 DOM 四次,可以被优化。
优化后:
var el = document.getElementById('mydiv');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';
另一个一次性改变风格的办法是修改 CSS 的类名称,而不是修改内联风格代码。
- 当你需要对 DOM 元素进行多次修改时,你可以通过以下步骤减少重绘和重排的次数
- 从文档流中摘除该元素,例:distplay: none;
- 对其应用多重改变
- 将过程带回文档中,例:display: block;
此过程引发两次重排 —— 第一步引发一次,第三步引发一次。
有三种基本方法可以将 DOM 从文档中摘除:
- 隐藏元素,进行修改,然后再显示它
使用一个文档片段在已存 DOM 之外创建一个子树,然后将它拷贝到文档中。推荐涉及最少数量的 DOM 操作和重排。
var fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
document.getElementById('mylist').appendChild(fragment);
将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素
var old = document.getElementById('mylist');
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
old.parentNode.replaceChild(clone, old);
- 最好是尽量减少对布局信息的查询次数,查询时将它赋给局部变量,并用局部变量参与计算。
重排有时只影响渲染树的一小部分,但也可以影响很大的一部分,甚至整个渲染树。浏览器需要重排的部分越小,应用程序的响应速度就越大。所以当一个页面顶部的动画推移了差不多整个页面时,将引发巨大的重排动作,使用户感到卡顿。渲染树的大多数节点需要被重新计算。
使用以下步骤可以避免对大部分页面进行重排:
- 使用绝对定位页面动画的元素,使它位于页面布局流之外。
- 启动元素动画。当它扩大时,它临时覆盖部分页面。这是一个重绘过程,但只影响页面的一小部分,避免重排并重绘一大块页面
- 当动画结束时,重新定位,从而只一次下移文档其它元素的位置。
即:
如果大量元素使用了 :hover 那么会降低反应速度。例如,如果你创建了一个由 500-1000 行 5 列构成的表,并使用 tr:hover 改变背景颜色,高亮显示鼠标光标所在的行,当鼠标光标在表上移动时,性能会降低。使用高亮是个慢速过程,CPU 使用率会提高到 80% - 90% 。所以当元素数量很多时避免使用这种效果,诸如很大的表或很长的列表。
事件委托
事件逐层冒泡并能被父级元素捕获。使用事件代理,只需给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有事件。
参考资料
- 《高性能JavaScript》