关于dom:DOM操作造成的页面卡顿问题及解决

50次阅读

共计 4743 个字符,预计需要花费 12 分钟才能阅读完成。

前言
界面上 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 操作的优化,所以利用如上的形式后可能并不能显著感触到性能的改善。然而在依然占有市场的一些旧浏览器中,利用以上这三种编码方式则能够大幅提高页面渲染性能。

  1. 设置具备动画成果的 DOM 元素的 position 属性为 fixed 或 absolute
    把页面中具备动画成果的元素设置为相对定位,使得元素脱离页面布局流,从而防止了页面频繁的回流,只波及动画元素本身的回流了。这种做法能够进步动 画成果的展现性能。如果把动画元素设置为相对定位并不合乎设计的要求,则能够在动画开始时将其设置为相对定位,等动画完结后复原原始的定位设置。在很多的 网站中,页面的顶部会有大幅的广告展现,个别会动画开展和折叠显示。如果不做性能的优化,这个成果的性能损耗是很显著的。应用这里提到的优化计划,则能够 进步性能。
  2. 审慎获得 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’;
复制代码

  1. 应用事件托管形式绑定事件
    在 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… 不胜感激!

正文完
 0