浏览器的重排和重绘
浏览器渲染
浏览器在渲染页面的时候,大致是以下几个步骤:
- 解析 html 生成 DOM 树,解析 css,生成 CSSOM 树,将 DOM 树和 CSSOM 树结合,生成渲染树;
- 根据渲染树,浏览器可以计算出网页中有哪些节点,各节点的CSS以及从属关系 - 回流
- 根据渲染树以及回流得到的节点信息,计算出每个节点在屏幕中的位置 - 重绘
- 最后将得到的节点位置信息交给浏览器的图形处理程序,让浏览器中显示页面
reflow 回流或重排 或者 layout
回流:英文叫reflow,指的是当渲染树中的节点信息发生了大小、边距等问题,需要重新计算各节点和css具体的大小和位置。
layout(reflow)一般被称为布局,这个操作是用来计算文档中元素的位置和大小,是渲染前重要的一步。在HTML第一次被加载的时候,会有一次layout之外,js脚本的执行和样式的改变同样会导致浏览器执行layout。
repaint 重绘 或者 paint
重绘:英文叫repaint,当节点的部分属性发生变化,但不影响布局,只需要重新计算节点在屏幕中的绝对位置并渲染的过程,就叫重绘。比如:改变元素的背景颜色、字体颜色等操作会造成重绘。
回流的过程在重绘的过程前面,所以回流一定会重绘,但重绘不一定会引起回流。
每次回流都会对浏览器造成额外的计算消耗,所以浏览器对于回流和重绘有一定的优化机制。浏览器通常都会将多次回流操作放入一个队列中,等过了一段时间或操作达到了一定的临界值,然后才会挨个执行,这样能节省一些计算消耗。但是在获取布局信息操作的时候,会强制将队列清空,也就是强制回流,比如访问或操作以下或方法时:
- offsetTop
- offsetLeft
- offsetWidth
- offsetHeight
- scrollTop
- scrollLeft
- scrollWidth
- scrollHeight
- clientTop
- clientLeft
- clientWidth
- clientHeight
- getComputedStyle()
这些属性或方法都需要得到最新的布局信息,所以浏览器必须去回流执行。因此,在项目中,尽量避免使用上述属性或方法,如果非要使用的时候,也尽量将值缓存起来,而不是一直获取。
减少回流和重绘
合并样式修改
减少造成回流的次数,如果要给一个节点操作多个css属性,而每一个都会造成回流的话,尽量将多次操作合并成一个
1var oDiv = document.querySelector('.box');
2oDiv.style.padding = '5px';
3oDiv.style.border = '1px solid #000';
4oDiv.style.margin = '5px';
- 使用 style 的 cssText
1oDiv.style.cssText = 'padding:5px; border:1px solid #000; margin:5px;';
- 使用 class
批量操作元素
当对DOM有多次操作的时候,需要使用一些特殊处理减少触发回流,其实就是对 DOM 的多次操作,在脱离标准流后,对元素进行的多次操作,不会触发回流,等操作完成后,再将元素放回标准流。
脱离标准流的操作有以下3种:
- 隐藏元素
display: none
- 使用文档碎片
createDocumentFragment
拷贝节点 有大病
HTMLCollection 问题
缓存选择器的结果,减少DOM查询。这里要特别提下 HTMLCollection。HTMLCollection 是通过 document.getElementByTagName
得到的对象类型,和数组类型很类似但是每次获取这个对象的一个属性,都相当于进行一次DOM查询:
1var divs = document.getElementsByTagName("div");
2for (var i = 0; i < divs.length; i++){ //infinite loop
3 document.body.appendChild(document.createElement("div"));
4}
比如上面的这段代码会导致无限循环,所以处理HTMLCollection对象的时候要做些缓存。
所有的 "getElementsBy*" 方法都会返回一个 实时的(live) 集合。这样的集合始终反映的是文档的当前状态,并且在文档发生更改时会“自动更新”。
在下面的例子中,有两个脚本。
第一个创建了对 <div> 的集合的引用。截至目前,它的长度是 1。
第二个脚本在浏览器再遇到一个 <div> 时运行,所以它的长度是 2。
1<div>First div</div>
2
3<script>
4 let divs = document.getElementsByTagName('div');
5 alert(divs.length); // 1
6</script>
7
8<div>Second div</div>
9
10<script>
11 alert(divs.length); // 2
12</script>
相反,querySelectorAll
返回的是一个 静态的 集合。就像元素的固定数组。
如果我们使用它,那么两个脚本都会输出 1
:
1<div>First div</div>
2
3<script>
4 let divs = document.querySelectorAll('div');
5 alert(divs.length); // 1
6</script>
7
8<div>Second div</div>
9
10<script>
11 alert(divs.length); // 1
12</script>
DOM 操作很慢,主要是读操作很慢
一般情况下,浏览器的layout是lazy的,也就是说:在js脚本执行时,是不会去更新DOM的,任何对DOM的修改都会被暂存在一个队列中,在当前js的执行上下文完成执行后,会根据这个队列中的修改,进行一次layout。
然而有时希望在js代码中立刻获取最新的DOM节点信息,浏览器就不得不提前执行layout,这是导致DOM性能问题的主因。
如下的操作会打破常规,并触发浏览器执行layout
- 通过js获取需要计算的DOM属性
- 添加或删除DOM元素
- resize浏览器窗口大小
- 改变字体
- css伪类的激活,比如:hover
- 通过js修改DOM元素样式且该样式涉及到尺寸的改变
部分读操作,导致 lazy reflow 失效这是导致 DOM 性能的主要原因