流必将引起重绘,重绘不一定会引起回流。
浏览器会把HTML解析成DOM,把CSS解析成CSSOM CSS Object Model即 CSSOM 和DOM合并就产生了Render Tree
当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流
会导致回流的操作:
页面首次渲染
浏览器窗口大小发生改变
元素尺寸或位置发生改变
元素内容变化(文字数量或图片大小等等)
元素字体大小变化
添加或者删除可见的DOM元素
激活CSS伪类(例如::hover)
查询某些属性或调用某些方法
一些常用且会导致回流的属性和方法:
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
!!!回流比重绘的代价要更高
当你访问以下属性或方法时,浏览器会立刻清空队列:
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
width、height
getComputedStyle()
getBoundingClientRect()
如何避免
CSS
避免使用table布局。
尽可能在DOM树的最末端改变class。
避免设置多层内联样式。
将动画效果应用到position属性为absolute或fixed的元素上。
避免使用CSS表达式(例如:calc())。
JavaScript
避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流
能优化中,减少重绘重排应该是一种很好的优化方式,我们具体看一下什么情况下会造成重绘重排,为什么减少重绘重排可以做到优化,怎么样减少重绘重排。
我们先看看当浏览器拿到服务端返回的资源时,是如何渲染的。
首先浏览器会进行文件解析,主要解析三个东西:
解析完成之后
布局完成之后,就要进行绘制了,将各层发给 GPU,GPU 将各层合成,显示在屏幕上,即 composite。
当我们开始绘制的时候,如果使用 js 操作了 dom 元素,或者改变了 css 属性,就可能会造成重绘(repaint)和重排(reflow)。
repaint:屏幕的一部分进行了重画,比如某个 css 中改变背景色,元素尺寸没有变。 reflow:任何一个元素的尺寸发生了变化,需要重新验证并计算 render tree,就会造成重排。
在 PC 时代,我们用 jquery 进行获取元素,改变元素的尺寸,及时发生重排,我们也很难感知到,但是当移动时代到来之后,如果频繁发生重排,那手机就会受不了了。
尤其是在执行下面操作时,成本会很高:
如果发生上述的行为基本都会造成重绘和重排。
所以,当发生重排时,一定会发生重绘,但是发生重绘不一定会发生重排。
浏览器中每个元素节点都有 reflow 方法,当一个元素发生 reflow 时,他的子节点都会发生 reflow。
举几个例子来说明一下造成重绘重排的情况:
var bodyStyle=document.body.style; // cache bodyStyle.padding='20px'; // reflow, repaint bodyStyle.border='10px solid red'; // 再一次的 reflow 和 repaint bodyStyle.color='blue'; // repaint bodyStyle.backgroundColor='#fad'; // repaint bodyStyle.fontSize='2em'; // reflow, repaint // new DOM element - reflow, repaint document.body.appendChild(document.createTextNode('children!'));
前面说了浏览器的渲染机制,多一次重绘就需要浏览器重新进行一次绘制,及时 GPU 处理会比较快,但是也是吃不消的,更别说重排了,重排一个 dom,会重新生成 render Tree,然后重新绘制。
其实浏览器很聪明,不可能每次修改样式就 reflow 或者 repaint 一次,一般来说,浏览器会积累一批操作,然后做一次 reflow。
但是也有些例外情况,比如 resize 窗口,改变窗口字体,浏览器会立即进行 reflow。
虽然浏览器会这么做,但是我们也应该减少重绘重排的次数,在开发阶段就为浏览器进行特殊的关爱,毕竟是每天陪伴我们的小伙伴。
下面总结了一些针对 reflow 和 repaint 的最佳实践:
当每次布局完成之后,就会发生 composite 过程,浏览器都把重绘后的图像发给 GPU 去合成并显示。
在上面最佳实践中最后提到了动画,动画其实是比较耗费性能的,因为动画的每一帧都会发给 GPU 去合成,重绘重排会发生在动画的每一帧。
我们在写动画的时候,可以通过 js 写,也可以通过 css 写。两种方式在写动画时,过程也是不一样的。
所以不难看出,耗费性能最少并能并最流畅的动画是只触发合成。
为了仅发生 composite,我们做动画的 css property 必须满足以下三个条件:
满足以上以上条件的 css property 只有 transform 和 opacity。
这样的话,由于没有重排和重绘,只有合成,那么浏览器在动画执行之前就知道动画如何开始和结束。
并且有两个优势:
事实上影响动画流畅性的因素不止重排重绘,还有 CPU 内存。
css 动画有一个重要的特性,它是完全工作在 GPU 上。因为你声明了一个动画如何开始和如何结束,浏览器会在动画开始前准备好所有需要的指令;并把它们发送给 GPU。
而如果使用 js 动画,浏览器必须计算每一帧的状态;为了保证平滑的动画,我们必须在浏览器主线程计算新状态;把它们发送给 GPU 至少 60 次每秒。
除了计算和发送数据比 css 动画要慢,主线程的负载也会影响动画; 当主线程的计算任务过多时,会造成动画的延迟、卡顿。
所以最佳实践中最后一条就提到了,在写动画时,尽量写 css 动画,并且尽量用 transform 和 opacity。
谷歌浏览器检测重绘工具:右上角三点->更多工具->开发者工具->Performance。
chrome浏览器的Performance是页面性能分析的利器,网上有很多关于关于如何去使用和查看Performance的文章,这里就不多做阐述了,大伙可以多去了解了解。
总之,页面性能优化是前端从初级到高级都避不开的一个话题,如何做到性能的最优化更是一个资深前端应该考虑的事情,这里也希望有更好更多见解的小伙伴能够私聊我,给我点意见。
文分享自华为云社区《前端页面之“回流重绘”-云社区-华为云》,作者:CoderBin。
在HTML中,每个元素都可以理解成一个盒子,在浏览器解析过程中,会涉及到回流与重绘:
具体的浏览器解析渲染机制如下所示:
在页面初始渲染阶段,回流不可避免的触发,可以理解成页面一开始是空白的元素,后面添加了新的元素使页面布局发生改变
当我们对 DOM 的修改引发了 DOM几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来
当我们对 DOM的修改导致了样式的变化(color或background-color),却并未影响其几何属性时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,这里就仅仅触发了回流
要想减少回流和重绘的次数,首先要了解回流和重绘是如何触发的
回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流,如下面情况:
还有一些容易被忽略的操作:获取一些特定属性的值
offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流
除此还包括getComputedStyle方法,原理是一样的
触发回流一定会触发重绘
可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边移到了右边,那我们要先确定好右边的具体位置,画好形状(回流),再画上它原有的颜色(重绘)
除此之外还有一些其他引起重绘行为:
由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列
当你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的offsetTop等方法都会返回最新的数据
因此浏览器不得不清空队列,触发回流重绘来返回正确的值
我们了解了如何触发回流和重绘的场景,下面给出避免回流的经验:
在使用 JavaScript 动态插入多个节点时, 可以使用DocumentFragment. 创建后一次插入. 就能避免多次的渲染性能
但有时候,我们会无可避免地进行回流或者重绘,我们可以更好使用它们
例如,多次修改一个把元素布局的时候,我们很可能会如下操作
const el=document.getElementById('el')
for(let i=0;i<10;i++) {
el.style.top=el.offsetTop + 10 + "px";
el.style.left=el.offsetLeft + 10 + "px";
}
每次循环都需要获取多次offset属性,比较糟糕,可以使用变量的形式缓存起来,待计算完毕再提交给浏览器发出重计算请求
// 缓存offsetLeft与offsetTop的值
const el=document.getElementById('el')
let offLeft=el.offsetLeft, offTop=el.offsetTop
// 在JS层面进行计算
for(let i=0;i<10;i++) {
offLeft +=10
offTop +=10
}
// 一次性将计算结果应用到DOM上
el.style.left=offLeft + "px"
el.style.top=offTop + "px"
我们还可避免改变样式,使用类名去合并样式
const container=document.getElementById('container')
container.style.width='100px'
container.style.height='200px'
container.style.border='10px solid red'
container.style.color='red'
使用类名去合并样式
<style>
.basic_style {
width: 100px;
height: 200px;
border: 10px solid red;
color: red;
}
</style>
<script>
const container=document.getElementById('container')
container.classList.add('basic_style')
</script>
前者每次单独操作,都去触发一次渲染树更改(新浏览器不会),
都去触发一次渲染树更改,从而导致相应的回流与重绘过程
合并之后,等于我们将所有的更改一次性发出
我们还可以通过通过设置元素属性display: none,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发回流与重绘,这个过程称为离线操作
点击下方,第一时间了解华为云新鲜技术~
华为云博客_大数据博客_AI博客_云计算博客_开发者中心-华为云
*请认真填写需求信息,我们会在24小时内与您取得联系。