相信所有前端开发都知道当我们在浏览器中请求一个html到浏览器渲染出这个html页面到底要经过哪些步骤,简单的说当浏览器获取到一个html文件并且渲染是需要经历三个步骤(不考虑dns解析和http请求过程,默认以获取html文件的情况)。

    1.首先页面会根据HTML的dom结构渲染出dom标签的树型结构我们称之为dom-tree。dom-tree里包含了所有的HTML标签,每个标签都是dom-tree里的一个node节点,其中包含了display:none的元素以及通过js动态添加的元素(前提是这个js已经执行)。

    2.在浏览器生成了dom-tree以后,会根据我们页面引入的css表文件,渲染一棵样式树,即css-tree,这个过程浏览器会自动去除一些浏览器不认识或者错误的样式,比如低端浏览器无法识别的css3样式,或者通过前缀-webkit-或者-moz-等当前浏览器无法识别的样式名以及不能识别的css hack。

    3.在dom-tree 与 css-tree 都解析完成以后,浏览器会将dom-tree 与css-tree一一对应生成一个最终用于页面渲染的树形结构,叫做 render-tree,浏览器就是通过这个render-tree来完成页面渲染,最终将html页面展示给用户的,注意:当前的render-tree 会过滤掉display:none;的隐藏元素,但不会过滤 visibility:hidden的元素。

    重绘与回流 

    在浏览器完成页面渲染最终将页面展示给用户以后,如果页面的dom元素的样式发生了变化,或者通过js添加或删除了dom元素后,就会发生页面的重绘与回流。

    重绘

    当一个dom结构的样式发生改变时(该样式不影响其他dom结构,如 color,background-color等),浏览器需要对该dom重绘,比如div背景从蓝色变成了红色,这时候浏览器必须重新渲染这个dom元素,才能展示给用户最新的视图结构。

    回流

    当一个dom结构的样式发生改变时(该变化将引起其他dom节点位置或属性变化,如尺寸,文字,位置的改变),浏览器不仅需要对该dom元素重绘,还将对该dom-tree节点以下部分都重新计算渲染,叫做回流,引起样式回流的操作主要包含以下几种情况:

  • 页面首次加载(无法避免)

  • 添加或者删除可见的dom元素

  • 元素的位置发生了变化(未脱离文档流)

  • 元素的尺寸发生变化(包括margin,padding,border)

  • 元素内容发生变化,比如文本节点改变,或者img的src属性改变

  • 页面窗口大小变化

    

    由回流与重绘的概念可知,回流一定会引起重绘,而重绘则不一定会触发回流(主要看是否影响其他dom结构)。

    现代浏览器的优化机制

    当然,随着浏览器的发展,对于视图渲染优化这方面浏览器也做了很多的优化。由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。

    但是!当我们获取布局相关信息的操作的时候,由于此时浏览器必须返回给我们当前视图的实时信息,因此浏览器必须执行并清空当前队列,获取最新的视图信息给我们。例如以下js方法:

  • offsetWidth, offsetHeight, offsetLeft, offsetTop

  • scrollTop, scrollLeft …

  • clientTop, clientLeft…

  • getComputedStyle()

  • getBrondClientRect()

  • 等…

    减少回流和重绘的优化

    以上我们了解了什么是重绘以及回流,那么我们如何减少回流呢?

    1. 我们可以将对元素多次的属性变化改成一次例如直接通过添加class的方式来改变样式:

// 该写法将触发三次回流(不考虑浏览器优化的情况,即使浏览器优化的情况)
const el = document.getElementById('test'); 
el.style.padding = '1px';
el.style.borderLeft = '1px';
el.style.borderRight = '1px';

// 该写法只会触发一次视图回流
const el = document.getElementById('test'); 
el.style.cssText += 'border-left: 1px; border-right: 1px; padding: 1px;';

// 或者
const el = document.getElementById('test'); 
el.classList.add('test') // 该样式如上添加的样式

    2. 将需要样式修改的元素如动画元素等,使用position:absolute 等方法,让其脱离文档流,这样该元素的样式改变了就不会影响其他的dom结构

    3. 将需要复杂操作的元素现在内存中进行操作,不去渲染它,在完成操作以后再去渲染:

const el = document.createElement('div');
el.style.padding = '1px';
el.style.borderLeft = '1px';
el.style.borderRight = '1px';
// 在对元素完成相关操作后再渲染
element.appendChild(div)

    4. 需要循环插入多条数据时,应该生成代码片段后再一次性插入,不要每次循环都插入一条数据。

    5. 我们知道render-tree只会处理display不为none的dom节点,因此如果要对一个dom节点做复杂操作可以先把该节点的display设置为none然后在进行相关操作,最后再将该节点设置为可见,这样只会触发两次的回流。

    6. 如上我们知道,当使用一些js方法的时候会强制触发回流,比如获取元素的width属性,此时我们可以将改属性缓存起来,而不是每次都去直接获取触发回流:

// 此时将触发10次回流
for (let i = 0; i < 10; i++) {
   list[i].style.width = box.offsetWidth + 'px';
}
// 此时只触发一次
let width = box.offsetWidth 
for (let i = 0; i < 10; i++) {
   list[i].style.width = width + 'px';
}

    GPU硬件加速技术

    比起减少回流, 直接不触发回流才是最好最完美的解决方案,此时GPU硬件加速就起到了一定性的作用。

    相信大家自css3 横空出世以来,就经常听到GPU硬件加速这个词,在制作一些css3动画或者一些特效时候,由于移动端一些机型的性能限制,动画卡顿时有发生,此时如果用到了gpu加速,会发现打开了新世界的大门,过渡效果明显流畅了!(关于硬件加速的原理大家自行百度,可以简单的理解为浏览器单独的开启一个渲染层单独渲染该dom)

    使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

    常见的触发硬件加速的css属性:

  • transform

  • opacity

  • filters

  • Will-change

    实际上直接使用transform属性也不会直接开启硬件加速的,我们可以用个设置transform:translateZ(0)的方式强制开启硬件加速。当然硬件加速也有很多相关的坑,在此不再赘述。

    以上,我们了解了什么是重绘与回流,以及避免多次页面回流的方法,由于回流对浏览器的性能影响很大,甚至影响js的性能,因此,在实际开发中,我们也应该尽量的避免多次的触发回流提升我们页面的性能。

By mikoshu

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注