关于回流与重绘优化的探索

50次阅读

共计 2750 个字符,预计需要花费 7 分钟才能阅读完成。

前言
杭州下雪了,冷到不行,在家躺在床上玩手机,打开微信进入前端交流群里日常吹水,看到大佬在群里发了一篇文章你应该要知道的重绘与重排,文章里有一段骚操作,就是为了减少重绘与重排,合并样式操作,这个骚操作成功的引起了我的注意,然后开启了我的探索。
正文
前言中描述的合并样式的骚操作是如下:
var el = document.querySelector(‘div’);
el.style.borderLeft = ‘1px’;
el.style.borderRight = ‘2px’;
el.style.padding = ‘5px’;
原文描述的大概意思是这段代码多次对 DOM 的修改和对样式的修改,页面会进行多次回流或者重绘,应该进行如下优化:
var el = document.querySelector(‘div’);
el.style.cssText = ‘border-left: 1px; border-right: 1px; padding: 5px;’
这样的优化在以前我刚开始学习前端的时候,经常也在一些相关的性能优化的文章里看到,因为一直没有探究过,概念里一直觉得自己应该把多次 DOM 的样式的修改合并在一起,这样效率会更高,直到后来,自己对浏览器的进程与线程慢慢有了了解,曾经也写过一篇博客,浅谈浏览器多进程与 JS 线程,其中有一个概念是,JS 线程与 GUI 渲染线程是互斥关系,大概的意思就是当 js 引擎在执行 js 代码的时候,浏览器的渲染引擎是被冻结了的,无法渲染页面的,必须等待 js 引擎空闲了才能渲染页面。
这个概念,JS 线程与 GUI 渲染线程是互斥关系与上面描述的骚操作似乎有点冲突,也就是当我们对 el.style 进行一系列赋值的时候,渲染引擎是被冻结的状态,怎么会进行多次重绘或者回流?带着这样的疑问,写了一个小 demo,代码如下。
<!DOCTYPE html>
<html>
<head>
<title> 测试页 </title>
<style>
#box {
width: 109px;
height: 100px;
background-color: lightsteelblue;
border-style: solid;
}
</style>
</head>
<body>
<div id=”box”></div>
</body>
<script>
var box = document.getElementById(‘box’);
var toggle = 0;
var time = 500;
function toggleFun() {
var borderWidth = toggle ? 20 : 0;
var borderColor = toggle ? ‘coral’ : ‘transparent’;
if (toggle) {
box.style.borderWidth = ’50px’;
box.style.borderWidth = borderWidth + ‘px’;
box.style.borderColor = borderColor;
} else {
box.style.cssText = ‘border: ‘ + borderWidth + ‘px solid’ + borderColor;
}
toggle = toggle ? 0 : 1;
}
setInterval(toggleFun, time)
</script>
</html>
代码大概的意思就是定时以两种操作设置样式,收集浏览器的回流或者重绘次数。
打开 chrome 的开发者工具,切换到 Performance 选项卡,点击左上角的圆 ○,开始 record,等几秒后 stop,点击 Frames 查看 Event log 选项卡,内容如下:

大概可以看到,Recalculate Style -> Layout -> Update Layer Tree -> Paint -> Composite Layers 这个过程在循环进行,触发的目标代码是第 25 行代码合 29 行代码,也就是 box.style.borderWidth = ’50px’; 和 box.style.cssText = ‘border: ‘ + borderWidth + ‘px solid’ + borderColor;。
首先回顾一下浏览器渲染页面的流程:

请求拿到 html 报文。
同时解析生成 CSS 规则树和 DOM 树。
合并 CSS 规则树和 DOM 树,生成 render 树。
渲染进程根据 render 树进行 Layout。
绘制 paint 页面。

然后在看看上面的过程,可以容易看出,

首先,Recalculate Style,重新计算 css 规则树。
进行 Layout,这里的 Layout 可以理解成回流,重新计算每个元素的位置。

Update Layer Tree,字面意思理解,更新层级树。

Paint,绘制页面,在这里可以理解成重绘。

Composite Layers,字面意思理解,合并层级。

由上面过程得到结果,当在同一执行任务里面对 DOM 的样式进行多次操作的时候,只会进行一次回流或者重绘,也就是说,只要我们的 js 引擎时候忙碌的,渲染引擎是冻结的时候,无论对 DOM 样式进行多少次操作,都只会进行一次回流或者重绘,也就是说前面说的合并样式优化是无效的。
这个时候,我对上面过程又产生了新的疑问,为什么要 Paint 之后在 Composite Layers 呢?为什么不把所有层合并完了在绘制页面呢?
…………………….(看搜索相关资料去了)
翻看资料结束后,我得到以下理解。
首先理解 layer 概念,可以理解成 PS 里面的图层,我们知道 PS 文件最后保存层 PSD 文件,当图层越多的时候,PSD 文件就越大,在我们的浏览器里面也是一样的,我们的 layer 越多,所占的内存就越大。
然后理解 Paint 真正做的事情,paint 的任务大概就是把所有的 layer 绘制到页面中,这个绘制与 canvas 的绘制不一样,canvas 的绘制相当于在画布里把像素直接绘制成指定颜色,然后我们直接看到的东西就直接是像素颜色,而我们这里说的 Paint 只是把图层丢到页面中,最后的绘制,需要交给 Composite 线程处理。
最后是 Composite Layers,由 composite 线程进行,这个线程在浏览器的 Renderer 进程中,任务是把 Paint 时候丢上页面的图层转化成位图,最终生成我们肉眼可以看到的图像,所以,真正的绘制,应该是 Composite Layers 过程进行的。
由于 paint 与 composite 解耦,浏览器对每一个 layer 都有一个标识,这个标识用来标识该 layer 是否需要重绘,在有 CSS 规则树变化的时候,浏览器只会对这些被标识的 layer 进行重绘,用这样的方式提高浏览器的渲染性能。
最后
前端大法博大精深,越往下学越觉得自己不适合前端!!!仿佛看到自己在从入门到跑路这条路上快走到了终点。。。
参考

Chrome 性能调优简介
浏览器解析过程
GPU:合成加速

正文完
 0