先谈两点
回流必将引起重绘,重绘不一定会引起回回流;
回流的代价要远大于重绘;
回流
当 Render Tree
中部分或全部节点, 因元素的尺寸、布局、隐藏等改变而需要重新构建,计算它们在设备视口 (viewport) 内的确切位置和大小,浏览器重新渲染的过程称为 回流。
会导致回流的操作:
- 页面首次渲染。
- 浏览器窗口大小发生改变。
- 元素尺寸或者位置发生改变。
- 元素内容变化(文字数量或者图片大小发生改变)。
- 元素字体大小的改变。
- 添加或者删除可见的
DOM
元素。 - 激活
CSS
伪类 (eg::hover
)。 - 查询某些属性或调用某些方法。
一些常用且会导致回流的属性和方法。
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
scrollIntoView()
、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
重绘
当页面中元素样式(例如:color
、background-color
等)的改变并不影响它周围或内部布局(尺寸、大小等)时,浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
容易造成重绘操作的css
- 颜色
- 边框样式 border-style
- 圆角 border-radius
- 文本修饰 text-decoration
- 阴影 box-shadow
- 轮廓线 outline
- 背景 background
注意
异步reflow
有些情况下,比如修改了元素的样式,浏览器并不会立刻reflow 或 repaint 一次,浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一定的临界值,浏览器就会挨个执行,进行一次批处理,将队列清空,这样可以把多次回流和重绘变成一次,这又叫 异步reflow
或 增量异步reflow
。
立即reflow
在有些情况下(获取布局信息操作的时候),会强制将队列清空,也就是强制回流,比如resize 窗口,改变了页面默认的字体等;
当访问以下属性或方法时,浏览器会立刻清空队列, 马上 reflow
:
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
scrollIntoView()
、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
css3不需要回流和重绘的属性:
- transform // 向元素应用 2D 或 3D 转换。该属性允许我们对元素进行旋转、缩放、移动或倾斜
- opacity // 设置元素的不透明级别
- filter // 定义了元素(通常是
)的可视效果(例如:模糊与饱和度)
- Will-change // 提前通知浏览器我们要对元素做什么动画,这样浏览器可以提前准备合适的优化设置
浏览器绘制 DOM
1、获取 DOM 并将其分割为多个层(layer)
2、将每个层独立地绘制进位图(bitmap)中
3、将层作为纹理(texture)上传至 GPU
4、复合(composite)多个层来生成最终的屏幕图像。
通常情况下,浏览器会将一个层的内容先绘制进一个位图中,然后再作为纹理(texture)上 传到 GPU,只要该层的内容不发生改变,就没必要进行重绘(repaint),浏览器会通过重新复合(recomposite)来形成一个新的帧。
transform
不重绘,不回流 是因为transform
属于合成属性,对合成属性进行transition/animate
动画时,将会创建一个合成层(composite layer)。这使得动画元素在一个独立的层中进行渲染。当元素的内容没有发生改变,就没有必要进行重绘。浏览器会通过重新复合来创建动画帧。
优化 减少回流
没有定位变化,只显隐
尽量将元素设置为visibility
,因为display:none会引起回流,而visibility
只会引起重绘。
避免使用 table
布局
尽量不要使用table
布局。如果没有定宽,表格一列的宽度由最宽的一列决定,那么很可能在最后一行的宽度超出之前的列宽,引起整体回流造成table可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。
对于复杂动画效果,使用绝对定位让其脱离文档流
对于复杂动画效果,由于会经常的引起回流重绘,尽量将元素设置为绝对定位absolute或者fixed,使得脱离文档流。操作元素的定位属性时,只有这一个元素会回流,不然容易引起父元素以及后续元素频繁的回流。
合并样式修改
减少造成回流的次数,如果要给一个节点操作多个css属性,而每一个都会造成回流的话,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性(而不是利用js修改单独每一个样式)。
批量操作DOM
当对DOM有多次操作的时候,需要使用一些特殊处理减少触发回流,在脱离标准流后,对元素进行的多次操作,不会触发回流。等操作完成后,再将元素放回标准流。
脱离标准流的操作有以下3种:
- 隐藏元素(display: none,只引发两次回流和重绘)
- 使用文档碎片(createDocumentFragment:创建一个虚拟的 documentFragment 节点)
- 拷贝节点
例:下面对DOM节点的多次操作,每次都会触发回流
var data = [
{
id:1,
name:"商品1",
},
{
id:2,
name:"商品1",
},
{
id:3,
name:"商品1",
},
{
id:4,
name:"商品1",
},
// 假设后面还有很多
];
var oUl = document.querySelector("ul");
for(var i=0;i<data.length;i++){
var oLi = document.createElement("li");
oLi.innerText = data[i].name;
oUl.appendChild(oLi);
}
这样每次给ul中新增一个li的操作,每次都会触发回流。
方法一:隐藏ul后,给ul添加节点,添加完成后再将ul显示
oUl.style.display = 'none';
for(var i=0;i<data.length;i++){
var oLi = document.createElement("li");
oLi.innerText = data[i].name;
oUl.appendChild(oLi);
}
oUl.style.display = 'block';
此时,在隐藏ul和显示ul的时候,触发了两次回流,给ul添加每个li的时候没有触发回流。
方法二:创建文档碎片,将所有li先放在文档碎片中,等都放进去以后,再将文档碎片放在ul中
var fragment = document.createDocumentFragment();
for(var i=0;i<data.length;i++){
var oLi = document.createElement("li");
oLi.innerText = data[i].name;
fragment.appendChild(oLi);
}
oUl.appendChild(fragment);
DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象. 与 Document
最大的区别是因为 DocumentFragment
不是真实 DOM
树的一部分,它的变化不会触发 DOM
树的(重新渲染) ,且不会导致性能等问题。
使用 document.createDocumentFragment
方法或者构造函数来创建一个空的 DocumentFragment
.
方法三:将ul拷贝一份,将所有li放在拷贝中,等都放进去以后,使用拷贝(深拷贝)替换掉ul。
var newUL = oUl.cloneNode(true);
for(var i=0;i<data.length;i++){
var oLi = document.createElement("li");
oLi.innerText = data[i].name;
newUL.appendChild(oLi);
}
oUl.parentElement.replaceChild(newUl, oUl);
注意
cloneNode()
代表浅拷贝
—标签里的元素,事件没有复制。
cloneNode(true)
代表深拷贝
—完全把真节点的东西给复制了过来
避免多次触发布局
获取一个元素的scrollTop、scrollLeft、scrollWidth、offsetTop、offsetLeft、offsetWidth、offsetHeight之类的属性,浏览器为了保证值的正确会回流取得最新的值,所以如果要多次操作,最好取完做个缓存。
如下回到顶部的操作:
goBack.onclick = function(){
setInterval(function(){
var t = document.documentElement.scrollTop || document.body.scrollTop;
t += 10;
document.documentElement.scrollTop = document.body.scrollTop = t;
},20)
}
每隔20毫秒都会重新获取滚动过的距离,每次都会触发回流,代码优化如下:
goBack.onclick = function(){
var t = document.documentElement.scrollTop || document.body.scrollTop;
setInterval(function(){
t += 10;
document.documentElement.scrollTop = document.body.scrollTop = t;
},20)
}
只获取一次,每次都让数字递增,避免每次都获取滚动过的距离。