前言
界面上 UI 的更改都是通过 DOM 操作实现的,并不是通过传统的刷新页面实现 的。只管 DOM 提供了丰盛接口供内部调用,但 DOM 操作的代价很高,页面前端代码的性能瓶颈也大多集中在 DOM 操作上,所以前端性能优化的一个次要的关注 点就是 DOM 操作的优化。
DOM 操作优化的总准则是尽量减少 DOM 操作。
先来看看 DOM 操作为什么会影响性能?
在浏览器中,DOM 的实现和 ECMAScript 的实现是拆散的。比方 在 IE 中,ECMAScrit 的实现在 jscript.dll 中,而 DOM 的实现在 mshtml.dll 中;在 Chrome 中应用 WebKit 中的 WebCore 解决 DOM 和渲染,但 ECMAScript 是在 V8 引擎中实现的,其余浏览器的状况相似。所以通过 JavaScript 代码调用 DOM 接 口,相当于两个独立模块的交互。相比拟在同一模块中的调用,这种跨模块的调用其性能损耗是很高的。但 DOM 操作对性能影响最大其实还是因为它导致了浏览器 的重绘(repaint)和回流(reflow)。
这里咱们先理解下浏览器的渲染原理:
从下载文档到渲染页面的过程中,浏览器会通过解析 HTML 文档来构建 DOM 树,解析 CSS 产生 CSS 规定树。JavaScript 代码在解析过程中,可能会批改生成的 DOM 树和 CSS 规定树(这也是为什么经常把 js 放在页面底部最初才渲染的起因)。之后依据 DOM 树和 CSS 规定树构建渲染树,在这个过程中 CSS 会依据选择器匹配 HTML 元素。渲染树包含了每 个元素的大小、边距等款式属性,渲染树中不蕴含暗藏元素及 head 元素等不可见元素。最初浏览器依据元素的坐标和大小来计算每个元素的地位,并绘制这些元 素到页面上。重绘指的是页面的某些局部要从新绘制,比方色彩或背景色的批改,元素的地位和尺寸并没用扭转;回流则是元素的地位或尺寸产生了扭转,浏览器需 要从新计算渲染树,导致渲染树的一部分或全副发生变化。渲染树从新建设后,浏览器会从新绘制页面上受影响的元素。回流的代价比重绘的代价高很多,重绘会影 响局部的元素,而回流则有可能影响全副的元素。如下的这些 DOM 操作会导致重绘或回流:
减少、删除和批改可见 DOM 元素
页面初始化的渲染
挪动 DOM 元素
批改 CSS 款式,扭转 DOM 元素的尺寸
DOM 元素内容扭转,使得尺寸被撑大
浏览器窗口尺寸扭转
浏览器窗口滚动
如何防止或者解决 DOM 操作造成的页面卡顿问题
1. 合并屡次的 DOM 操作为单次的 DOM 操作
最常见频繁进行 DOM 操作的是频繁批改 DOM 元素的款式,代码相似如下:
element.style.borderColor = ‘#f00’;
element.style.borderStyle = ‘solid’;
element.style.borderWidth = ‘1px’;
复制代码
这种编码方式会因为频繁更改 DOM 元素的款式,触发页面屡次的回流或重绘,下面介绍过,古代浏览器针对这种状况有性能的优化,它会合并 DOM 操作,但并不是所有的浏览器都存在这样的优化。举荐的形式是把 DOM 操作尽量合并,如上的代码能够优化为:
// 优化计划 1
element.style.cssText += ‘border: 1px solid #f00;’;
// 优化计划 2
element.className += ’empty’;
复制代码
示例的代码有两种优化的计划,都做到了把屡次的款式设置合并为一次设置。计划 2 比计划 1 略微有一些性能上的损耗,因为它须要查问 CSS 类。但计划 2 的维护性最好,这在上一章已经探讨过。很多时候,如果性能问题并不突出,抉择编码方案时须要优先思考的是代码的维护性。
相似的操作还有通过 innerHTML 接口批改 DOM 元素的内容。不要间接通过此接口来拼接 HTML 代码,而是以字符串形式拼接好代码后,一次性赋值给 DOM 元素的 innerHTML 接口。
2. 把 DOM 元素离线或暗藏后批改
把 DOM 元素从页面流中脱离或暗藏,这样解决后,只会在 DOM 元素脱离和增加时,或者是暗藏和显示时才会造成页面的重绘或回流,对脱离了页面布局流的 DOM 元素操作就不会导致页面的性能问题。这种形式适宜那些须要大批量批改 DOM 元素的状况。具体的形式次要有三种:
(1)应用文档片段
文档片段是一个轻量级的 document 对象,并不会和特定的页面关联。通过在文档片段上进行 DOM 操作,能够升高 DOM 操作对页面性能的影响,这 种形式是创立一个文档片段,并在此片段上进行必要的 DOM 操作,操作实现后将它附加在页面中。对页面性能的影响只存在于最初把文档片段附加到页面的这一步 操作上。代码相似如下:
var fragment = document.createDocumentFragment();
// 一些基于 fragment 的大量 DOM 操作
…
document.getElementById(‘myElement’).appendChild(fragment);
复制代码
(2)通过设置 DOM 元素的 display 款式为 none 来暗藏元素
这种形式是通过暗藏页面的 DOM 元素,达到在页面中移除元素的成果,通过大量的 DOM 操作后复原元素原来的 display 款式。对于这类会引起页面重绘或回流的操作,就只有暗藏和显示 DOM 元素这两个步骤了。代码相似如下:
var myElement = document.getElementById(‘myElement’);
myElement.style.display = ‘none’;
// 一些基于 myElement 的大量 DOM 操作
…
myElement.style.display = ‘block’;
复制代码
(3)克隆 DOM 元素到内存中
这种形式是把页面上的 DOM 元素克隆一份到内存中,而后再在内存中操作克隆的元素,操作实现后应用此克隆元素替换页面中原来的 DOM 元素。这样一来,影响性能的操作就只是最初替换元素的这一步操作了,在内存中操作克隆元素不会引起页面上的性能损耗。代码相似如下:
var old = document.getElementById(‘myElement’);
var clone = old.cloneNode(true);
// 一些基于 clone 的大量 DOM 操作
…
old.parentNode.replaceChild(clone, old);
复制代码
在古代的浏览器中,因为有了 DOM 操作的优化,所以利用如上的形式后可能并不能显著感触到性能的改善。然而在依然占有市场的一些旧浏览器中,利用以上这三种编码方式则能够大幅提高页面渲染性能。
- 设置具备动画成果的 DOM 元素的 position 属性为 fixed 或 absolute
把页面中具备动画成果的元素设置为相对定位,使得元素脱离页面布局流,从而防止了页面频繁的回流,只波及动画元素本身的回流了。这种做法能够进步动 画成果的展现性能。如果把动画元素设置为相对定位并不合乎设计的要求,则能够在动画开始时将其设置为相对定位,等动画完结后复原原始的定位设置。在很多的 网站中,页面的顶部会有大幅的广告展现,个别会动画开展和折叠显示。如果不做性能的优化,这个成果的性能损耗是很显著的。应用这里提到的优化计划,则能够 进步性能。 - 审慎获得 DOM 元素的布局信息
后面探讨过,获取 DOM 的布局信息会有性能的损耗,所以如果存在反复调用,最佳的做法是尽量把这些值缓存在局部变量中。思考如下的一个示例:
for (var i=0; i < len; i++) {
myElements[i].style.top = targetElement.offsetTop + i*5 + 'px';
}
复制代码
如上的代码中,会在一个循环中重复获得一个元素的 offsetTop 值,事实上,在此代码中该元素的 offsetTop 值并不会变更,所以会存在不必要的性能损耗。优化的计划是在循环内部获得元素的 offsetTop 值,相比拟之前的计划,此计划只是调用了一遍元素的 offsetTop 值。更改后的代码如下:
var targetTop = targetElement.offsetTop;
for (var i=0; i < len; i++) {
myElements[i].style.top = targetTop+ i*5 + 'px';
}
复制代码
另外,因为获得 DOM 元素的布局信息会强制浏览器刷新渲染树,并且可能会导致页面的重绘或回流,所以在有大批量 DOM 操作时,应防止获取 DOM 元素 的布局信息,使得浏览器针对大批量 DOM 操作的优化不被毁坏。如果须要这些布局信息,最好是在 DOM 操作之前就获得。思考如下一个示例:
var newWidth = div1.offsetWidth + 10;
div1.style.width = newWidth + ‘px’;
var newHeight = myElement.offsetHeight + 10; // 强制页面回流
myElement.style.height = newHeight + ‘px’; // 又会回流一次
复制代码
依据下面的介绍,代码在遇到获得 DOM 元素的信息时会触发页面从新计算渲染树,所以如上的代码会导致页面回流两次,如果把获得 DOM 元素的布局信息提前,因为浏览器会优化间断的 DOM 操作,所以实际上只会有一次的页面回流呈现,优化后的代码如下:
var newWidth = div1.offsetWidth + 10;
var newHeight = myElement.offsetHeight + 10;
div1.style.width = newWidth + ‘px’;
myElement.style.height = newHeight + ‘px’;
复制代码
- 应用事件托管形式绑定事件
在 DOM 元素上绑定事件会影响页面的性能,一方面,绑定事件自身会占用解决工夫,另一方面,浏览器保留事件绑定,所以绑定事件也会占用内存。页面中 元素绑定的事件越多,占用的解决工夫和内存就越大,性能也就绝对越差,所以在页面中绑定的事件越少越好。一个优雅的伎俩是应用事件托管形式,即利用事件冒 泡机制,只在父元素上绑定事件处理,用于解决所有子元素的事件,在事件处理函数中依据传入的参数判断事件源元素,针对不同的源元素做不同的解决。这样就不 须要给每个子元素都绑定事件了,治理的事件绑定数量变少了,天然性能也就进步了。这种形式也有很大的灵活性,能够很不便地增加或删除子元素,不须要思考因 元素移除或改变而须要批改事件绑定。示例代码如下:
// 获取父节点,并增加一个 click 事件
document.getElementById(‘list’).addEventListener(“click”,function(e) {// 查看事件源元素 if(e.target && e.target.nodeName.toUpperCase == “LI”) {// 针对子元素的解决 …
}
});
复制代码
上述代码中,只在父元素上绑定了 click 事件,当点击子节点时,click 事件会冒泡,父节点捕捉事件后通过 e.target 查看事件源元素并做相应地解决。在 JavaScript 中,事件绑定形式存在浏览器兼容问题,所以在很多框架中也提供了类似的接口办法用于事件托管。比方在 jQuery 中能够应用如下形式实现事件的托管(示例代码来自 jQuery 官方网站):
$(“table”).on(“click”, “td”, function() {$( this).toggleClass(“chosen”);
});
复制代码
最初
如果你感觉此文对你有一丁点帮忙,点个赞。或者能够退出我的开发交换群:1025263163 互相学习,咱们会有业余的技术答疑解惑
如果你感觉这篇文章对你有点用的话,麻烦请给咱们的开源我的项目点点 star: https://gitee.com/ZhongBangKe… 不胜感激!