前端面试题之:什么是浏览器重排(回流)与重绘,如何优化浏览器性能?

发布于2023-08-26 18:00 阅读 212

浏览器渲染

浏览器在渲染页面的时候,大致是以下几个步骤:

1、解析html生成DOM树,解析css,生成CSSOM树,将DOM树和CSSOM树结合,生成渲染树;

2、根据渲染树,浏览器可以计算出网页中有哪些节点,各节点的CSS以及从属关系 - 回流

3、根据渲染树以及回流得到的节点信息,计算出每个节点在屏幕中的位置 - 重绘

4、最后将得到的节点位置信息交给浏览器的图形处理程序,让浏览器中显示页面

回流(重排)

回流:英文叫reflow,指的是当渲染树中的节点信息发生了大小、边距等问题,需要重新计算各节点和css具体的大小和位置。例:在css中对一个div修饰的样式中,使用了宽度50%,此时需要将50%转换为具体的像素,这个计算的过程,就是回流的过程。

容易造成回流的css


width
height
padding
border
margin
position
top
left
bottom
right
float
clear
text-align
vertical-align
line-height
font-weight
font-size
font-family
overflow
white-space

重绘

重绘:英文叫repaint,当节点的部分属性发生变化,但不影响布局,只需要重新计算节点在屏幕中的绝对位置并渲染的过程,就叫重绘。比如:改变元素的背景颜色、字体颜色等操作会造成重绘。回流的过程在重绘的过程前面,所以回流一定会重绘,但重绘不一定会引起回流。

容易造成重绘的css


color
border-style
border-radius
text-decoration
box-shadow
outline
background


每次回流都会对浏览器造成额外的计算消耗,所以浏览器对于回流和重绘有一定的优化机制。浏览器通常都会将多次回流操作放入一个队列中,等过了一段时间或操作达到了一定的临界值,然后才会挨个执行,这样能节省一些计算消耗。但是在获取布局信息操作的时候,会强制将队列清空,也就是强制回流,比如访问或操作以下或方法时:


offsetTop
offsetLeft
offsetWidth
offsetHeight
scrollTop
scrollLeft
scrollWidth
scrollHeight
clientTop
clientLeft
clientWidth
clientHeight
getComputedStyle()

这些属性或方法都需要得到最新的布局信息,所以浏览器必须去回流执行。因此,在项目中,尽量避免使用上述属性或方法,如果非要使用的时候,也尽量将值缓存起来,而不是一直获取。


减少回流和重绘

合并样式修改

减少造成回流的次数,如果要给一个节点操作多个css属性,而每一个都会造成回流的话,尽量将多次操作合并成一个,例:


<script>
  let oDiv = document.querySelector('.box');
  oDiv.style.padding = '5px';
  oDiv.style.border = '1px solid #000';
  oDiv.style.margin = '5px';
</script>

操作div的3个css属性,分别是padding、border、margin,此时就可以考虑将多次操作合并为一次。

方法一:使用style的cssText


<script>
  let oDiv=document.querySelector('.box');
  oDiv.style.cssText='padding:5px;border:1px solid #000;margin:5px;';
</script>


方法二:将这几个样式定义给一个类名,然后给标签添加类名

<style>
    .pbm{
        padding:5px; 
        border:1px solid #000; 
        margin:5px;
    }
</style>
<script>
    let oDiv = document.querySelector('.box');
    oDiv.classList.add('pbm');
</script>


批量操作DOM

当对DOM有多次操作的时候,需要使用一些特殊处理减少触发回流,其实就是对DOM的多次操作,在脱离标准流后,对元素进行的多次操作,不会触发回流,等操作完成后,再将元素放回标准流

脱离标准流的操作有以下3中:

  1. 1、隐藏元素
  2. 2、使用文档碎片
  3. 3、拷贝节点

例:下面对DOM节点的多次操作,每次都会触发回流


<script>
let data = [
    {
        id:1,
        name:"商品1",
    },
    {
        id:2,
        name:"商品2",
    },
    {
        id:3,
        name:"商品3",
    },
    {
        id:4,
        name:"商品4",
    },
];
let oUl = document.querySelector("ul");
for(let i=0;i<data.length;i++){
    let oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    oUl.appendChild(oLi);
}
</script>

这样每次给ul中新增一个li的操作,每次都会触发回流。


方法一:隐藏ul后,给ul添加节点,添加完成后再将ul显示


<script>
  let oUl = document.querySelector("ul");
  oUl.style.display = 'none';
  for(let i=0;i<data.length;i++){
      let oLi = document.createElement("li");
      oLi.innerText = data[i].name;
      oUl.appendChild(oLi);
  }
  oUl.style.display = 'block';
</script>

此时,在隐藏ul和显示ul的时候,触发了两次回流,给ul添加每个li的时候没有触发回流。


方法二:创建文档碎片,将所有li先放在文档碎片中,等都放进去以后,再将文档碎片放在ul中


<script>
let oUl = document.querySelector("ul");
let fragment = document.createDocumentFragment();
for(let i=0;i<data.length;i++){
    let oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    fragment.appendChild(oLi);
}
oUl.appendChild(fragment);
</script>

方法三:将ul拷贝一份,将所有li放在拷贝中,等都放进去以后,使用拷贝替换掉ul


<script>
let oUl = document.querySelector("ul");
let newUL = oUl.cloneNode(true);
for(let i=0;i<data.length;i++){
    let oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    newUL.appendChild(oLi);
}
oUl.parentElement.replaceChild(newUl, oUl);
</script>


关注通哥每周学点前端小知识,微信公众号搜索《每周小知识》。

评论

全部评论

{{item.username}}
{{item.comment_content}}
回复

{{reply.username}} 回复:{{reply.replyname}}

{{reply.content}}

{{reply.isshow ? '取消回复' : '回复'}}

作者

程序员李蛋

一个早早地学会让自己的灵魂充分扩展的人,以后才能将整个世界收入自己的灵魂当中。