重排重绘

2/25/2023 css

# 前言

首先说明下网页HTML文件变成屏幕上画面过程:

  1. HTML内容被HTML解析器解析成DOM树
  2. CSS内容被CSS解析器解析成CSSOM树
  3. DOM树+CSSOM树会产生Render Tree(渲染树)
  4. 生成布局。浏览器根据渲染树来布局,以计算每个节点的几何信息(位置、大小)
  5. 将各个节点绘制到屏幕上 如下图

浏览器渲染机制

其中第四步为布局排列(flow),第五步为绘制(paint)。这两步加起来就是我们通常所说的渲染

今天要介绍的就是Reflow(重排)、重绘(Repaint),有的人说是回流与重绘

# 一.是什么?

在HTML中,每个元素可以理解为一个盒子,在浏览器解析过程中,会设计重排重绘:

  • 重排:==元素的位置发生变动==(原因)时产生重排,也叫回流。计算每一个元素在设备视口内的确切位置和大小(这是what)。当一个位置发生改变时其父元素以及后面的元素都可能发生变化,代价极高
  • 重绘:元素的样式发生改变,但是位置没有发生改变(原因)。将渲染树的每个节点转换成屏幕上的实际像素(重点),这一步通常称为绘制或者栅格化

在页面初始渲染阶段,重排不可避免的触发。可以理解成页面一开始是空白的元素,后面添加了新的元素,使页面布局发生改变

# 一.如何触发?

重排触发时机

重排一定会触发重绘

  • 添加或删除可见dom元素

  • 元素位置发生改变,或者动画

  • 元素尺寸发生改变(外边框、内边框、边框大小、高度宽度等)

  • 内容发生改变

  • 页面一开始渲染的时候

  • 浏览器窗口改变(重排是根据视口的大小来计算位置大小)(resize事件发生时) 还有一些容易忽略的操作:获取一些特定属性的值

  • offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight,getComputedStyle()方法

这些属性有一个特性,需要通过既是计算得到。因此浏览器为了获取这些值,也会进行回流(重排)

重绘触发时机

重绘不一定会触发重排

我们可以把页面理解为一个黑板,黑板上有一朵画好了的小花。现在我们要把这躲小花从左边移到右边,那我们首先要确定好右边的具体位置,再画好形状(重排),再画上它原有的颜色(重绘)

引起重绘的行为:

  1. 样色修改
  2. 阴影的修改

# 三.如何减少?

避免重排的经验:

  • 如果设置元素样式,通过改变元素的class类名
  • 避免设置多项内联样式
  • 应用元素的动画,使用position属性的fixed或者absoluted,尽可能使元素脱离文档流,减少对其他元素的影响
  • 避免使用table布局。table中某个元素大小以及内容的改变,都会引起整个table的重新计算
  • 使用css3硬件加速(GPU)。可以让transform、opcity、filters不会引起重排重绘
  • 避免使用css的JavaScript表达式
  • 减少http请求次数

# 四.参考回答?

如果被问到这个问题,大概要怎么回答? `

重排和重绘是浏览器渲染上的两个关键节点,html会被html解析器解析成DOM树,css会被css解析器解析成CSSOM树,两者形成一个渲染树,然后根据渲染树,来布局,确定页面上所有内容的位置大小(重排),然后把像素绘制到屏幕上

其中重排就是当元素位置改变时,浏览器重新执行布局这个步骤,来重新确定页面上内容的大小和位置,确定完之后再重新绘制到屏幕上,所以重排一定会引起重绘

如果元素没有发生变动,仅仅是样式变了,这时候浏览器渲染的时候就会跳过布局的步骤,直接进入绘制步骤,这就是重绘,所以重绘不一定会引起重排

# 五.例子

例如,多次修改一个把元素布局的时候,我们很可能会如下操作

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,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发回流与重绘,这个过程称为离线操作

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

离线操作后

let container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了许多类似的后续操作)
container.style.display = 'block'

参考文献

如何回答如何理解重排和重绘 (opens new window) 你真的了解回流和重绘吗 (opens new window) 回流与重绘 (opens new window)