# 前言
首先说明下网页HTML文件变成屏幕上画面过程:
- HTML内容被HTML解析器解析成DOM树
- CSS内容被CSS解析器解析成CSSOM树
- DOM树+CSSOM树会产生Render Tree(渲染树)
- 生成布局。浏览器根据渲染树来布局,以计算每个节点的几何信息(位置、大小)
- 将各个节点绘制到屏幕上 如下图
其中第四步为布局排列(flow),第五步为绘制(paint)。这两步加起来就是我们通常所说的渲染
今天要介绍的就是Reflow(重排)、重绘(Repaint),有的人说是回流与重绘
# 一.是什么?
在HTML中,每个元素可以理解为一个盒子,在浏览器解析过程中,会设计重排重绘:
- 重排:==元素的位置发生变动==(原因)时产生重排,也叫回流。计算每一个元素在设备视口内的确切位置和大小(这是what)。当一个位置发生改变时其父元素以及后面的元素都可能发生变化,代价极高
- 重绘:元素的样式发生改变,但是位置没有发生改变(原因)。将渲染树的每个节点转换成屏幕上的实际像素(重点),这一步通常称为绘制或者栅格化
在页面初始渲染阶段,重排不可避免的触发。可以理解成页面一开始是空白的元素,后面添加了新的元素,使页面布局发生改变
# 一.如何触发?
重排触发时机
重排一定会触发重绘
添加或删除可见dom元素
元素位置发生改变,或者动画
元素尺寸发生改变(外边框、内边框、边框大小、高度宽度等)
内容发生改变
页面一开始渲染的时候
浏览器窗口改变(重排是根据视口的大小来计算位置大小)(resize事件发生时) 还有一些容易忽略的操作:获取一些特定属性的值
offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight,getComputedStyle()方法
这些属性有一个特性,需要通过既是计算得到。因此浏览器为了获取这些值,也会进行回流(重排)
重绘触发时机
重绘不一定会触发重排
我们可以把页面理解为一个黑板,黑板上有一朵画好了的小花。现在我们要把这躲小花从左边移到右边,那我们首先要确定好右边的具体位置,再画好形状(重排),再画上它原有的颜色(重绘)
引起重绘的行为:
- 样色修改
- 阴影的修改
# 三.如何减少?
避免重排的经验:
- 如果设置元素样式,通过改变元素的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)