前言
DOM(Document Object Model)是一个与语言无关的、用来操作XMl和HTML文档的应用程序接口。在浏览器环境下,我们是通过Javascript来实现对DOM的操作。而针对DOM进行频繁操作会严重影响前端的体验和性能,下面我们就简单谈一下如何针对这种情况进行一些优化。
浏览器工作原理
先通过一张图来看一下浏览器是如何进行页面画渲染的:
首先通过HTML文件生成DOM Tree和CSS规则生成CSS Rule Tree,然后Javascript可以通过DOM API和CSSOM API对生成后的DOM Tree和CSS Rule Tree进行需要的操作。接下来CSS Rule Tree中的规则应用到DOM Tree上,构建出Rendering Tree。最后调用操作系统Native GUI的API绘制。
当Rendering Tree中的元素尺寸大小,布局位置,显示隐藏等改变时,需要对Rending Tree进行重新构建,这个过程就称为回流。回流完成后浏览器会重新绘制受影响的部分。回流必引起重绘,而重绘不一定引起回流。
天生就慢的DOM如何优化
实际上Javascipt的执行速度虽然比不上C、C++等预编译语言,但是和访问、操作DOM�比起来那还是要快很多的。当你的Rendering Tree足够大时,每次使用Javascript调用DOM API去访问一个DOM元素,都会执行一个遍历过程,而当你又对获取的元素进行了操作,这就又会触发回流和重绘,这些都会大量消耗性能,很容易造成页面闪烁,卡顿等,影响用户体验。所以我们沿着这个思路就能找到要优化的点。
1. 使用尽可能少的DOM元素来构建页面
页面中过多的DOM元素,会使定位元素变得缓慢。当触发回流和重绘操作时会消耗更多的性能。还有就是在后期的维护上也会增加不必要的复杂性。如果页面真的很复杂,精简DOM不可行,可以用局部变量缓存获取过的DOM元素:
function cacheDOM() {
// 缓存DOM节点,避免在循环中不断重新获取
let div = document.getElementsByTagName('div'),
let len = div.length;
for(let i = 0; i < len; i++) {
console.log(div.nodeType);
}
}
还可以使用一些高效的API,优先使用document.getElementById
,而document.querySelector
和document.querySelectorAll
在性能上和getElementsBy*
API相比较慢。
2. 减少回流和重绘
在操作DOM元素时,除了获取过程,还有对元素的外观,布局等进行的修改。触发回流的操作有以下类型:
添加或者删除可见的DOM元素
改变元素的位置
元素的内边距、外边距、边框、宽度、高度等尺寸的改变
元素文本内容和图片大小的改变导致高度和宽度的重新计算
浏览器窗口尺寸的改变
-
页面初始化渲染
var s = document.body.style; s.padding = "2px"; // 回流+重绘 s.border = "1px solid red"; // 再一次 回流+重绘 s.color = "blue"; // 再一次重绘 s.backgroundColor = "#ccc"; // 再一次 重绘 s.fontSize = "14px"; // 再一次 回流+重绘 // 添加node,再一次 回流+重绘 document.body.appendChild(document.createTextNode('abc!'));
通过上面的代码可以看出回流和重绘频繁发生。
而在实际的浏览器渲染过程中,会对上述过程做优化。连续的回流和重绘会被放入一个队列中,当任务积累到一定数量或者一定的时间间隔,就会刷新队列。减少了多次渲染的性能消耗。但是有些操作,浏览器为了返回正确的值,会忽略优化,立刻执行回流和重绘操作,比如:offsetTop
、scorllTop
、clientTop
、width
等。
如何减少DOM的回流和重绘呢?我们可以从以下几个方面入手:
a. 在动态修改CSS样式时,不要逐条设置,可以使用类名
element.style.fontWeight = 'bold' ;
element.style.marginLeft= '30px' ;
element.style.marginRight = '30px' ;
// 可以优化为如下代码,可以将多次回流减少到一次
element.className = 'selectedAnchor' ;
b. 对DOM的操作,留在Javascript内部,然后一次写回DOM,即实现离线处理
第一种,可以使用DocumentFragment的机制:DocumentFragment接口表示没有父级的最小文档对象,由于文档片段不是实际DOM结构的一部分,它是一个虚拟的dom节点,存在于内存中,所以对片段所做的更改不会影响文档,导致回流,影响性能。
function CreateFragments(){
var fragment = document.createDocumentFragment();
for(var i = 0;i < 10000;i++){
var tmpNode = document.createElement("div");
tmpNode.innerHTML = "test" + i + "<br />";
fragment.appendChild(tmpNode);
}
document.body.appendChild(fragment);
}
第二种,使用使用cloneNode在外部更新节点然后再通过replace与原始节点互换,也只触发一次回流重绘。
var orig = document.getElementById('container');
var clone = orig.cloneNode(true);
var list = ['foo', 'bar', 'baz'];
var content;
for (var i = 0; i < list.length; i++) {
content = document.createTextNode(list[i]);
clone.appendChild(content);
}
orig.parentNode.replaceChild(clone, orig);
还有一种方法,比较简单,将要操作的DOM设置为dispaly:none
,操作完成后再设置为display:block
,这样只触发了两次回流。
c. 对于动画元素,尽可能使其脱离文档流,使用fixed
和absolute
定位。在进行动画制作时,如果能用opcity和transform实现,就不要使用其他属性。这两个属性只会触发composite,而不会触发回流。
总结
本文从浏览器的工作原理触发,简单说明了一下DOM慢的原因,然后给出了优化DOM操作的几个建议。主要目的就是为了精简DOM结构、减少DOM的回流和重绘。如果要对页面的性能瓶颈进行详细的分析,可以借助Chrome开发工具中的Performance面板。找到具体的原因,针对性的提出优化方案。