背景:DOM操作占用CPU比较多,导致浏览器重新渲染,耗时,所以避免频繁的DOM操作
JavaScript DOM操作的效率是很低的,而且不是一般的慢,而且这也是引发性能问题的常见问题之一。为什么会慢呢?因为对DOM的修改为影响网页的用户界面,重绘页面是最昂贵的浏览器操作之一。
在讨论页面重绘、重排之前。需要对页面的呈现流程有些了解,页面是怎么把html结合css等显示到浏览器上的,下面的流程图显示了浏览器对页面的呈现的处理流程。可能不同的浏览器略微会有些不同。但基本上都是类似的。
- 浏览器把获取到的HTML代码解析成一个DOM树,HTML中的每个tag都是DOM树中的节点,根节点就是我们常用的document对象。DOM树里包含了所有HTML标签,包括display:none隐藏,还有用JS动态添加的元素等。
- 浏览器把所有样式(用户定义的CSS和用户代理)解析成样式结构体,在解析的过程中会去掉浏览器不能识别的样式,比如IE会去掉-moz开头的样式,而FF会去掉_开头的样式。
- DOM Tree 和样式结构体组合后构建render tree,render tree能识别样式,render tree中每个节点都有自己的样式,而且 render tree不包含隐藏的节点 (比如display:none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 render tree中。注意 visibility:hidden隐藏的元素还是会包含到 render tree中的,因为visibility:hidden 会影响布局(layout),会占有空间。
- 一旦render tree构建完毕后,浏览器就可以根据render tree来绘制页面了。
回流与重绘
当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。
当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。
注意:回流必将引起重绘,而重绘不一定会引起回流。
回流何时发生:
当页面布局和几何属性改变时就需要回流。下述情况会发生浏览器回流:
- 添加或者删除可见的DOM元素;
- 元素位置改变;
- 元素尺寸改变——边距、填充、边框、宽度和高度
- 内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
- 页面渲染初始化;
- 浏览器窗口尺寸改变——resize事件发生时; DOM操作带来的页面重绘或重排是不可避免的,但可以遵循一些最佳实践来降低由于重排或重绘带来的影响。
合并多次的DOM操作为单次的DOM操作
最常见频繁进行DOM操作的是频繁修改DOM元素的样式,代码类似如下:
element.style.color = "red";
element.style.fontSize = "20px";
element.style.background = "blue";
element.innerHtml = 'a'
element.innerHtml += 'b'
这种编码方式会因为频繁更改DOM元素的样式,触发页面多次的重排或重绘,建议可以把这些样式放到一个class里面,减少对DOM的操作:
.exstyle{ color: red; font-size: 20px; background: blue; }
//在js中换成这种写法 element.className = "exstyle";
var html = 'a'
html += 'b'
element.innerHtml = html
对DOM查询做缓存
for (let i = 0; i < document.getElementsByTagName('p').length; i++) {
//每次循环,都会计算length,频繁进行DOM操作
}
//------------------------------------------
//缓存 DOM 查询结果
const pList=document.getElementsByTagName('p')
const length =pList.length
for (let i = 0; i < length; i++) {
//缓存length,只进行一次DOM查询
}
将频繁的操作改为一次性操作
//频繁操作
const list=document.getElementById('list')
for (let i = 0; i < 10; i++) {
const li=document.createElement('li')
li.innerHTML=`List item ${i}`
list.appendChild(li) //插入 10 次
}
// ------------------------------------------
//通过创建一个新的空白的文档片段(DocumentFragment),最后一次插入
const list=document.getElementById('list')
const frag=document.createDocumentFragment()
for (let i = 0; i < 10; i++) {
const li=document.createElement('li')
li.innerHTML=`List item ${i}`
frag.appendChild(li) //插入 10 次
}
//都完成后,再传DOM树中
list.appendChild(frag)
DocumentFragments描述: DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。 因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。
通过设置DOM元素的display样式为none来隐藏元素
由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发两次重排。
var myElement = document.getElementById('myElement');
myElement.style.display = 'none';
// 一些基于myElement的大量DOM操作
...
myElement.style.display = 'block';
克隆DOM元素到内存中
这种方式是把页面上的DOM元素克隆一份到内存中,然后再在内存中操作克隆的元素,操作完成后使用此克隆元素替换页面中原来的DOM元素。这样一来,影响性能的操作就只是最后替换元素的这一步操作了,在内存中操作克隆元素不会引起页面上的性能损耗。
var old = document.getElementById('myElement');
var clone = old.cloneNode(true);
// 一些基于clone的大量DOM操作
...
old.parentNode.replaceChild(clone, old);
设置具有动画效果的DOM元素的position属性为fixed或absolute
把页面中具有动画效果的元素设置为绝对定位,使得元素脱离页面布局流,从而避免了页面频繁的重排,只涉及动画元素自身的重排了。这种做法可以提高动 画效果的展示性能。如果把动画元素设置为绝对定位并不符合设计的要求,则可以在动画开始时将其设置为绝对定位,等动画结束后恢复原始的定位设置。在很多的 网站中,页面的顶部会有大幅的广告展示,一般会动画展开和折叠显示。如果不做性能的优化,这个效果的性能损耗是很明显的。使用这里提到的优化方案,则可以 提高性能。
谨慎取得DOM元素的布局信息
-
缓存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操作,所以实际上只会有一次的页面重排出现.
var newWidth = div1.offsetWidth + 10; var newHeight = myElement.offsetHeight + 10; div1.style.width = newWidth + 'px'; myElement.style.height = newHeight + 'px';