做过前端开发的小伙伴就算不是非常理解重排与重绘,但是肯定都听过这两个词。那为什么这两个东西这么重要?因为他与我们的页面性能息息相关,今天,我们就来好好研究一下这两个东西。
在讲解重排和重绘之前,我们有必要说一下浏览器的渲染流程。下面是浏览器渲染过程中最关键的几个部分。如果想了解完整的浏览器渲染流程,推荐大家去阅读李兵老师的浏览器工作原理实践,需要付费阅读。或者参考我的这篇博文:一文让你彻底搞懂浏览器的渲染流程
其中,重排和重绘影响的就是其中的布局和绘制过程。
简单来说,涉及元素的几何更新时,叫重排。而只涉及样式更新而不涉及几何更新时,叫重绘。对于两者来说,重排必定引起重绘,但是重绘并不一定引起重排。所以,当涉及重排时,浏览器会将上述的步骤再次执行一遍。当只涉及重绘时,浏览器会跳过Layout步骤,即:
而如果既不需要重排,也不需要重绘,那么就是下面这样:
浏览器会直接跳到合成阶段。显然,对于页面性能来说,不重排也不重绘 > 重绘 > 重排。
显然,触发重排的一般都是几何因素,这是比较好理解的:
还有其他一些操作也可能引发重排
我们可能不太理解为什么这些操作也能引起重排,这里我先简单解释一下。因为现在的浏览器已经非常完善了,会自动帮我们做一些优化。当我们用js操作DOM的时候,浏览器并不是立马执行的,而是将操作存储在一个队列中。当达到一定数量或者经过一定时间以后浏览器再统一的去执行队列中的操作。那么回到我们刚才的问题,为什么查询这些属性也会导致重排?因为当你查询这些属性时,浏览器就会强制刷新队列,因为如果不立马执行队列中的操作,有可能得到的结果就是错误的。所以相当于你强制打断了浏览器的优化流程,引发了重排。下面我们通过一些小例子来进一步理解这段话:
首先我们来一个显然会引发重排的操作
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<style>
#test {
width: 100px;
height: 100px;
background-color: red;
position: relative;
}
style>
head>
<body>
<div id="test">
div>
<button onclick="reflow()">clickbutton>
<script>
function reflow() {
var div = document.querySelector("#test");
div.style.left = '200px';
}
script>
body>
html>
当我们点击按钮时,将div的left设置为200px,很显然,这个操作会引发浏览器的重排。下面我们用chrome自带的performence工具分析一下左移的过程。
可以看到这时已经触发了点击事件,此时我们的div.style.left='200px’的代码显然已经生效了,但是浏览器并没有立马更新布局,我们再往后看
把时间轴往后拉,可以看到这几个过程,先简单介绍一些这些名词代表的含义:
这里只做一个简单的介绍,对其中内容不太明白的同学可以参考李兵老师的文章或者参考我介绍浏览器渲染流程的博客。
那通过这个图我们可以看到,我们改变了div的left之后就触发了Layout,即重排的过程。下面我们仅改变div的背景颜色,给大家一个对比。
可以看到,如果仅仅改变背景颜色,是没有Layout这个过程的。同时,chrome还提供了一个工具rendering,我们后面也会用到这个工具,如果最顶部找不到的话可以去more tools中找到。
当你把第一个选项勾上以后,页面中涉及重新渲染的操作都会被绿色的框框表示出来,可以很方便的看到什么元素被重新渲染了。但是这样辨别不了重排和重绘的区别。
下面我们回到最初的问题,我们获取offsetLeft属性时是如何引发重排的?
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<style>
#test {
width: 100px;
height: 100px;
background-color: red;
position: relative;
}
style>
head>
<body>
<div id="test">
div>
<button onclick="reflow()">clickbutton>
<script>
function reflow() {
var div = document.querySelector("#test");
console.log(div.offsetLeft);
}
script>
body>
html>
我们用chrome工具看看渲染的过程
当我们点击按钮之后,浏览器立马重新计算了CSSOM,和我们之前的分析一致,浏览器需要强制更新队列。但是在后续的过程中,我们也并没有看到Layout过程,这是因为我们实际上并没有改变几何元素,所以本次操作并没有产生重排。(红框上方的 reflow 为我们的函数名)
那么我们再看下面的代码:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<style>
#test {
width: 100px;
height: 100px;
background-color: red;
position: relative;
}
style>
head>
<body>
<div id="test">
div>
<button onclick="reflow()">clickbutton>
<script>
function reflow() {
var div = document.querySelector("#test");
div.style.left = '200px';
console.log(div.offsetLeft);
}
script>
body>
html>
我们加了一行代码,改变了他的left,我们再来看看会产生什么效果
点击事件发生后,浏览器立马进行了重排,并没有像以前一样经过一段时间才产生重排。看到这里大家应该明白了,获取 offset 等属性是否会产生重排取决于之前是否有改变几何因素的操作。但是,不论怎样,它还是会重新计算 CSSOM 。那么有的同学可能会问了,所以我使用offsetLeft仅仅是把重排提前了,并没有造成什么实际的影响。对于这种情况而言是这样的,那么我们来看看下面两段代码的区别:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<style>
#test {
width: 100px;
height: 100px;
background-color: red;
position: relative;
}
style>
head>
<body>
<div id="test">
div>
<button onclick="reflow()">clickbutton>
<script>
function reflow() {
var div = document.querySelector("#test");
div.style.left = '200px';
console.log(div.offsetLeft);
div.style.left = '100px';
console.log(div.offsetLeft);
div.style.left = '200px';
console.log(div.offsetLeft);
div.style.left = '100px';
console.log(div.offsetLeft);
}
script>
body>
html>
这个操作会产生几次重排呢?我们来看一下:
因为每次获取属性的时候都需要更新队列,所以一共是产生了四次重排,如果我们修改一下代码的顺序:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<style>
#test {
width: 100px;
height: 100px;
background-color: red;
position: relative;
}
style>
head>
<body>
<div id="test">
div>
<button onclick="reflow()">clickbutton>
<script>
function reflow() {
var div = document.querySelector("#test");
div.style.left = '200px';
div.style.left = '100px';
div.style.left = '200px';
div.style.left = '100px';
console.log(div.offsetLeft);
console.log(div.offsetLeft);
console.log(div.offsetLeft);
console.log(div.offsetLeft);
}
script>
body>
html>
我们再来看看过程
可以看到,只发生了一次重排,reflow的时间比原先少了将近一半。原因很简单:因为第一个console.log会清空之前的四次操作,但是后面队列中已经没有新的操作加入,自然后面三个就不会引发重排。所以可以看出,在代码中合理的使用offset等属性还是非常有必要的。
同时,在其他博客中,我留意到这几种属性也能引发重排:
但是经过我的尝试,如果单纯的改变背景颜色等非几何属性,是不会引发重排的。而如果是去改变几何属性,那么我觉得就可以归结到最开始的几种情况中去。所以对于这块内容我持怀疑态度。如果有同学了解的希望在评论区指出。
说完了重排和重绘,不要忘记我们最开始提到的,最高效的方式就是跳过重排和重绘阶段。你可能会想,什么情况下可以做到这一点?其实这就是我们平时说的GPU加速,具体是如何实现呢?在开发过程中,如果我们使用了某些属性,浏览器会帮助我们将使用了该属性的div提升到一个单独的合成层,而在后面的渲染中,提升到该层的div将跳过重排和重绘的操作,直接到合成阶段。在stack overflow上有问题提到了这块内容。我们翻译一下就是:
下面几个属性能让浏览器帮助我们将div提升到一个单独的合成层:
最后一点是我加上去的,同时根据文中的内容我们可以知道,css3硬件加速是浏览器的行为,所以在不同浏览器下可能会有不同的表现形式。下面我们用一个例子来理解一下。这是李兵老师在他的专栏中提出的一个例子,我拿过来借用一下,注意box中的will-change属性:
<html>
<head>
<title>观察will-changetitle>
<style>
.box {
will-change: transform, opacity;
display: block;
float: left;
width: 40px;
height: 40px;
margin: 15px;
padding: 10px;
border: 1px solid rgb(136, 136, 136);
background: rgb(187, 177, 37);
border-radius: 30px;
transition: border-radius 1s ease-out;
}
body {
font-family: Arial;
}
style>
head>
<body>
<div id="controls">
<button id="start">startbutton>
<button id="stop">stopbutton>
div>
<div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
<div class="box">旋转盒子div>
div>
<script>
let boxes = document.querySelectorAll('.box');
let boxes1 = document.querySelectorAll('.box1');
let start = document.getElementById('start');
let stop = document.getElementById('stop');
let stop_flag = false
start.addEventListener('click', function () {
stop_flag = false
requestAnimationFrame(render);
})
stop.addEventListener('click', function () {
stop_flag = true
})
let rotate_ = 0
let opacity_ = 0
function render() {
if (stop_flag)
return 0
rotate_ = rotate_ + 6
if (opacity_ > 1)
opacity_ = 0
opacity_ = opacity_ + 0.01
let command = 'rotate(' + rotate_ + 'deg)';
for (let index = 0; index < boxes.length; index++) {
boxes[index].style.transform = command
boxes[index].style.opacity = opacity_
}
requestAnimationFrame(render);
}
script>
body>
html>
chrome工具为我们提供了查看是否提升了层的方法
选中layers在选中第二个图标,就可以用鼠标查看3d视图,我们可以看到每个div都被提升到了单独的层。如果我们把will-change去掉再来查看会是下面这样:
可以看到他们变成全部都在同一层了。那么加不加这个属性到底有没有实际的意义呢?我们下面实际检测一下。首先再介绍一下上文提到过的rendering工具
首先我们不加will-change来实现动画,可以看到如下效果
我们的每个div都在不断的重新渲染,同时fps是在30左右,大家自己去尝试就会发现已经能够感到明显的卡顿。我们再加上will-change尝试一下:
我们可以看到黄框取代了绿框,证明这些小球已经没有重新渲染了,而是被提升到了单独的层,同时fps稳定在60,能够直接感受到比原先顺畅了许多。
通过这个简单的例子,我们就能直观的感受到跳过重排和重绘带来的好处。我们也日常开发中也可以使用
来欺骗浏览器,让其帮助我们将div提升到一个单独的层。不过要注意,该方法虽好,但是也会加大内存的消耗,如果将太多没有必要的层单独提升了反而会造成页面的卡顿,具体可以参考一下这篇文章。
而关于这块更详细的介绍可以参考淘系前端团队分享的文章 无线性能优化:Composite。
好了,这就是今天的全部内容,看到的小可爱可以帮忙点个赞和关注,让我有继续写下去的动力~同时文中有什么错误的地方也欢迎大家在评论区指出,共同探讨。