性能优化 - 重排和重绘
性能优化 - 重排和重绘
重排 & 重绘
DOM的变化影响了元素的几何属性,浏览器需要重新计算元素的几何属性,同时其他元素的几何属性和位置也会受到影响,浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树,这个过程是重排,浏览器会重新绘制受到影响的部分到屏幕,这个过程叫重绘。
- 重排(Reflow):当DOM的变化引发了元素几何属性的变化,比如改变元素的宽高,元素的位置,导致浏览器不得不重新计算元素的几何属性,并重新构建渲染树。
- 重绘(Repaint):是在一个元素的外观被改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。比如改变某个元素的背景色、文字颜色、边框颜色等等
概述
- DOM树
表示页面的结构 - 渲染树
表示页面的节点如何显示
一旦渲染树构建完成,就要开始绘制(paint)页面元素了。当DOM的变化引发了元素几何属性的变化,比如改变元素的宽高,元素的位置,导致浏览器不得不重新计算元素的几何属性,并重新构建渲染树,这个过程称为“重排”。完成重排后,要将重新构建的渲染树渲染到屏幕上,这个过程就是“重绘”。简单的说,重排负责元素的几何属性更新,重绘负责元素的样式更新。而且,重排必然带来重绘,但是重绘未必带来重排。比如,改变某个元素的背景,这个就不涉及元素的几何属性,所以只发生重绘。
重点
- 重绘不一定需要重排(比如颜色的改变),重排必然导致重绘(比如改变网页位置)
重排触发机制
- 改变元素几何属性
- 添加或删除可见的DOM元素
- 元素位置改变
- 元素本身的尺寸发生改变(外边距、内边距、边框厚度、宽高、等几何属性)
- 内容改变
- 页面渲染器初始化
- 浏览器窗口大小发生改变
- 获取某些属性。当获取一些属性时,浏览器为取得正确的值也会触发重排,它会导致队列刷新,这些属性包括:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle() (currentStyle in IE)。所以,在多次使用这些值时应进行缓存。
优化
重绘和重排的开销是非常昂贵的,如果我们不停的在改变页面的布局,就会造成浏览器耗费大量的开销在进行页面的计算,这样的话,我们页面在用户使用起来,就会出现明显的卡顿。
浏览器优化
现在的浏览器其实已经对重排进行了优化,比如如下代码:
浏览器自己的优化:
浏览器会维护1个队列,把所有会引起重排,重绘的操作放入这个队列,等队列中的操作到一定数量或者到了一定时间间隔,浏览器就会flush队列,进行一批处理,这样多次重排,重绘变成一次重排重绘
var div = document.querySelector('.div');
div.style.width = '200px';
div.style.background = 'red';
div.style.height = '300px';
比较久远的浏览器,这段代码会触发页面2次重排,在分别设置宽高的时候,触发2次.
- 队列化修改并批量执行
当代的浏览器对此进行了优化,这种思路类似于现在流行的MVVM框架使用的虚拟DOM,对改变的DOM节点进行依赖收集,确认没有改变的节点,就进行一次更新。但是浏览器针对重排的优化虽然思路和虚拟DOM接近,但是还是有本质的区别。大多数浏览器通过队列化修改并批量执行来优化重排过程。也就是说上面那段代码其实在现在的浏览器优化下,只构成一次重排。
但是还是有一些特殊的元素几何属性会造成这种优化失效。比如:
offsetTop, offsetLeft,...
scrollTop, scrollLeft, ...
clientTop, clientLeft, ...
getComputedStyle() (currentStyle in IE)
为什么造成优化失效呢?仔细看这些属性,都是需要实时回馈给用户的几何属性或者是布局属性,当然不能再依靠浏览器的优化,因此浏览器不得不立即执行渲染队列中的“待处理变化”,并随之触发重排返回正确的值。
最小化重绘和重排
减少重排
合并所有的改变
// javascript
var el = document.querySelector('.el');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 这个例子其实和上面那个例子是一回事儿,在最糟糕的情况下,会触发浏览器三次重排。然鹅更高效的方式就是合并所有的改变一次处理。这样就只会修改DOM节点一次,比如改为使用cssText属性实现:
var el = document.querySelector('.el');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';
切换类名
不要一条一条地修改 DOM 的样式。可以先定义好 css 的 class,然后修改 DOM 的 className。
// css
.active {
padding: 5px;
border-left: 1px;
border-right: 2px;
}
// javascript
var el = document.querySelector('.el');
el.className = 'active';
改变样式最小化重绘和重排这种优化方式适用于单个存在的节点。
减少dom操作
不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
- 核心思想
- 让该元素脱离文档流
- 对其进行多重改变
- 将元素带回文档中
- 使元素可以脱离文档流的方法(注意,这里不使用css中的浮动&绝对定位)
- 隐藏元素,进行修改后,然后再显示该元素
- 使用文档片段创建一个子树,然后再拷贝到文档中
- 将原始元素拷贝到一个独立的节点中,操作这个节点,然后覆盖原始元素
// html
<ul id="mylist">
<li><a href="https://www.mi.com">xiaomi</a></li>
<li><a href="https://www.miui.com">miui</a></li>
</ul>
// javascript 现在需要添加带有如下信息的li节点
let data = [
{
name: 'tom',
url: 'https://www.baidu.com',
},
{
name: 'ann',
url: 'https://www.techFE.com'
}
]
首先,我们先写一个通用的用于将新数据更新到指定节点的方法:
// javascript
function appendNode($node, data) {
var a, li;
for(let i = 0, max = data.length; i < max; i++) {
a = document.createElement('a');
li = document.createElement('li');
a.href = data[i].url;
a.appendChild(document.createTextNode(data[i].name));
li.appendChild(a);
$node.appendChild(li);
}
}
首先我们忽视所有的重排因素,大家肯定会这么写:
let ul = document.querySelector('#mylist');
appendNode(ul, data);
使用这种方法,在没有任何优化的情况下,每次插入新的节点都会造成一次重排(这几部分我们都先讨论重排,因为重排是性能优化的第一步)。考虑这个场景,如果我们添加的节点数量众多,而且布局复杂,样式复杂,那么能想到的是你的页面一定非常卡顿。我们利用批量修改DOM的优化手段来进行重构
隐藏元素,进行修改后,然后再显示该元素
let ul = document.querySelector('#mylist');
ul.style.display = 'none';
appendNode(ul, data);
ul.style.display = 'block';
这种方法造成俩次重排
,分别是控制元素的显示与隐藏。对于复杂的,数量巨大的节点段落可以考虑这种方法。为啥使用display属性呢,因为display为none的时候,元素就不在文档流了,还不熟悉的,手动Google一下,display:none, opacity: 0,visibility: hidden的区别
使用文档片段创建一个子树,然后再拷贝到文档中
let fragment = document.createDocumentFragment();
appendNode(fragment, data);
ul.appendChild(fragment);
文档片段是一个轻量级的document对象,它设计的目的就是用于更新,移动节点之类的任务,而且文档片段还有一个好处就是,当向一个节点添加文档片段时,添加的是文档片段的子节点群,自身不会被添加进去。不同于第一种方法,这个方法并不会使元素短暂消失造成逻辑问题。上面这个例子,只在添加文档片段的时候涉及到了一次重排
将原始元素拷贝到一个独立的节点中,操作这个节点,然后覆盖原始元素
let old = document.querySelector('#mylist');
let clone = old.cloneNode(true);
appendNode(clone, data);
old.parentNode.replaceChild(clone, old);
可以看到这种方法也是只有一次重排
。总的来说,使用文档片段,可以操作更少的DOM(对比使用克隆节点),最小化重排重绘次数。
替换高性能api
document.createDocumentFragment
replaceChild
暂存引用
缓存布局信息这个概念,在《高性能JavaScript》DOM性能优化中,多次提到类似的思想,比如我现在要得到页面ul节点下面的100个li节点,最好的办法就是第一次获取后就保存起来,减少DOM的访问以提升性能,缓存布局信息也是同样的概念。前面有讲到,当访问诸如offsetLeft,clientTop这种属性时,会冲破浏览器自有的优化————通过队列化修改和批量运行的方法,减少重排/重绘版次。所以我们应该尽量减少对布局信息的查询次数,查询时,将其赋值给局部变量,使用局部变量参与计算。
看以下样例:
将元素div向右下方平移,每次移动1px,起始位置100px, 100px。性能糟糕的代码:
div.style.left = 1 + div.offsetLeft + 'px';
div.style.top = 1 + div.offsetTop + 'px';
这样造成的问题就是,每次都会访问div的offsetLeft,造成浏览器强制刷新渲染队列以获取最新的offsetLeft值。更好的办法就是,将这个值保存下来,避免重复取值
current = div.offsetLeft;
div.style.left = 1 + ++current + 'px';
div.style.top = 1 + ++current + 'px';
fixed 或 absoult 的 position
为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
table
千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。(table及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。这也是为什么我们要避免使用table做布局的一个原因。)
查询
不要在布局信息改变的时候做查询(会导致渲染队列强制刷新)