根据浏览器渲染界面原理理解渲染阻塞、浏览器的重绘(repaints)与回流(reflows)

53次阅读

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

前面有讲到当用户在浏览器输入 url 之后,经过一系列的过程,会最终向服务器请求到文档数据,文档数据请求到之后,浏览器会将这些数据传给浏览器渲染引擎,渲染引擎开始正式工作了。
构建 dom 树,解析 css
首先浏览器接收到 html 文档,就会把 HTML 在内存中转换成 DOM 树,HTML 中的每个 tag 都是 DOM 树中的 1 个节点,根节点就是我们常用的 document 对象。DOM 树里包含了所有 HTML 标签,包括 display:none 隐藏,还有用 JS 动态添加的元素等。在转换的过程中如果发现某个节点 (node) 上引用了 CSS 或者 image,就会再次向服务器请求 css 或 image, 然后继续执行构建 dom 树的转换,而不需要等待请求的返回,当请求的 css 文件返回后,就会开始解析 css style,浏览器把所有样式 (用户定义的 CSS 和用户代理) 解析成样式结构体,在解析的过程中会去掉浏览器不能识别的样式,比如 IE 会去掉 -moz 开头的样式,而 FF 会去掉_开头的样式。

构建 render Tree 及绘制
DOM Tree 和样式结构体组合后构建 render tree,也就是渲染树。渲染树和 dom 树有很大的区别,render tree 中每个 NODE 都有自己的 style,而且 render tree 不包含隐藏的节点 (比如 display:none 的节点,还有 head 节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 render tree 中。注意 visibility:hidden 隐藏的元素还是会包含到 render tree 中的,因为 visibility:hidden 会影响布局(layout),会占有空间。根据 CSS2 的标准,render tree 中的每个节点都称为 Box (Box dimensions),理解页面元素为一个具有填充、边距、边框和位置的盒子。一旦 render tree 构建完毕后,浏览器就可以根据 render tree 来绘制页面了。
注意:由于浏览器的流布局,对渲染树的计算通常只需要遍历一次就可以完成。但 table 及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花 3 倍于同等元素的时间。这也是为什么我们要避免使用 table 做布局的一个原因。

渲染阻塞
在浏览器进行加载时,其实是并行加载所有资源。对于 css 和图片等资源,浏览器加载是异步的,并不会影响到后续的加载、html 解析和后续渲染。
css 阻塞渲染
由上面过程可以看到,页面布局是在渲染树构建好之后发生的,而渲染树依赖 css 样式结构体,所以 CSS 被视为阻塞渲染的资源(但不阻塞 html 的解析,不会阻塞 dom 树的构建),这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。
因为 css 会阻塞渲染,所以我们应该尽早的尽快地下载到客户端,以便缩短首次渲染的时间。平时在开发的时候,应注意以下几点:

将 CSS 放在 head,不管内联还是外联都尽早开始下载或者构建 CSSOM(前提是这个 CSS 是首屏必须的)
避免使用 CSS import,CSS 中可以用 import 将另一个样式表引入,不过这样会在构建 CSSOM 时会增加一次网络来回时间。
适度内联 CSS,衡量其他因素,如外联,看网络来回影响多大,考虑 css 文件的大小
全面考虑渲染情况,网速差、文件下载失败等,防止白屏时间太长

同时,还有以下优化点:
一、媒体查询
通过使用媒体查询,我们可以根据特定的需求(比如显示或打印),也可以根据动态情况(比如屏幕方向变化、尺寸调整事件等)定制外观,
<link href=”style.css” rel=”stylesheet”>
<link href=”print.css” rel=”stylesheet” media=”print”>
<link href=”other.css” rel=”stylesheet” media=”(min-width: 40em)”>
看上面的代码,第一行,这样的普通声明,会阻塞渲染第二行,这个声明,只在打印网页时应用,因此网页在浏览器中加载时,不会阻塞渲染。第三行,提供了由浏览器执行的“媒体查询”,只有符合条件时,样式表会生效,浏览器才会阻塞渲染,直至样式表下载并处理完毕。
二、preload
<link rel=”preload” href=”index_print.css” as=”style” onload=”this.rel=’stylesheet'”>
preload 是 resoure hint 规范中定义的一个功能,顾名思义预加载,将 rel 改为 preload 后,相当于加了一个标志位,浏览器解析的时候会提前建立连接或加载资源,做到尽早并行下载,然后在 onload 事件响应后将 link 的 rel 属性改为 stylesheet 即可进行解析。
IE chrome firefox 三者的差异

IE 只要看到 HTML 标签就会进行绘制
chrome 不管 css 放在前面还是后面,都要等到 CSSOM 构建形成后才会绘制到页面上
firefox 放在 head 则会阻塞绘制,放在 body 末尾会先绘制前面的标签

三、动态添加 link
var style = document.createElement(‘link’);
style.rel = ‘stylesheet’;
style.href = ‘index.css’;
document.head.appendChild(style);
js 动态添加 DOM 元素 link,不会阻塞渲染。loadCSS.js,CSS preload polyfill 第三方库,原理同上
四、代码简练

js 阻塞
js 可能会操作 html,css,由于浏览器不了解脚本计划在页面上执行什么操作,它会作最坏的假设并阻止解析器,也就是之前讲过浏览器的 GUI 线程与 js 引擎线程是互斥的。所以,js 会阻塞渲染
浏览器对于 js 脚本文件的加载,则会导致 html 解析和渲染停止,直至 js 脚本加载并执行完毕才继续,但是对于后续的非 js 资源加载并不会停止,浏览器会对后续资源进行预加载。而资源加载是属于另外单独的线程,所以 js 加载并不会影响其他非 js 资源的加载,是浏览器的机制。
总的来说就是以下几点:

js 脚本在文档中的位置很重要,因为其跟 html 和 css 有很强的依赖关系
在 HTML 解析器解析到 script 标签后,会停止 DOM 构建
javascript 可以操作 DOM 和 CSSOM,但进行这些行为时要确保相应 DOM 和 CSSOM 已经存在,
JavaScript 执行将暂停,直至 CSSOM 就绪

当 CSS 后面跟着嵌入的 JS 的时候,该 CSS 就会出现阻塞后面资源下载的情况,因为浏览器会维持 html 中 css 和 js 的顺序,样式表必须在嵌入的 JS 执行前先加载、解析完。而嵌入的 JS 会阻塞后面的资源加载,所以就会出现 CSS 阻塞下载的情况。
使用 chrome 浏览器的 performance 工具查看浏览器的渲染过程:
例如下面这段代码,看浏览器是如何一步步将界面绘制出来
<!DOCTYPE html>
<html lang=”zh-cn”>
<head>
<meta charset=”UTF-8″>
<meta http-equiv=”X-UA-Compatible” content=”IE=Edge”>
<meta name=”author” content=”Reddy.Huang, i@0u0b.com”/>
<title> 浏览器渲染 </title>
<link href=”./css/main.css” rel=”stylesheet”>

</head>
<body>
<div class=”wrap”>
<div class=”left”>
</div>
<div class=”middle”>
<div class=”line”>

</div>

</div>
<div class=”right”>
<p>fgdgg</p>
<p>fgdgg</p>
<p>fgdgg</p>
<p>fgdgg</p>
<p>fgdgg</p>
</div>
</div>

<script src=”./js/3.js”></script>

</body>
</html>

通过浏览器的工具上的可以很清楚的看到界面的渲染过程,也可以很清楚的看到请求加载资源的时候,不会对 html 解析造成影响,但如果资源加载过慢,会导致渲染阻塞,通过此图可以很好的理解浏览器的渲染机制
如果我把 js 放在 css 之后,如下代码:
<!DOCTYPE html>
<html lang=”zh-cn”>
<head>
<meta charset=”UTF-8″>
<meta http-equiv=”X-UA-Compatible” content=”IE=Edge”>
<meta name=”author” content=”Reddy.Huang, i@0u0b.com”/>
<title> 浏览器渲染 </title>
<link href=”./css/main.css” rel=”stylesheet”>
<script src=”./js/3.js”></script>

</head>
<body>
<div class=”wrap”>
<div class=”left”>
</div>
<div class=”middle”>
<div class=”line”>

</div>

</div>
<div class=”right”>
<p>fgdgg</p>
<p>fgdgg</p>
<p>fgdgg</p>
<p>fgdgg</p>
<p>fgdgg</p>
</div>
</div>

</body>
</html>
再次查看浏览器的渲染过程:

图中可以明显的看出,首先浏览器开始解析 html,然后再解析的过程中遇到 css,开始加载 css 资源,遇到 js 开始加载 js 资源,当 css 加载完成后,开始解析 css,js 加载完成后,则开始解析 js,此时解析 html 生成 dom 树会停止,直到 js 解析完成之后,才再次开始解析 html,重新计算样式,布局,生成渲染树,最终才是界面绘制,所以在开发的时候不要将 js 文件写在头部,这样会影响界面的绘制,导致界面出现空白
浏览器的重绘(repaints)与回流(reflows)
重绘当 render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color。则就叫称为重绘。
回流当 render tree 中的一部分 (或全部) 因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。
回流必然会造成重绘,重绘不会造成回流。
回流何时发生:
当页面布局和几何属性改变时就需要回流。下述情况会发生浏览器回流:
1、添加或者删除可见的 DOM 元素;
2、元素位置改变;
3、元素尺寸改变——边距、填充、边框、宽度和高度
4、内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
5、页面渲染初始化;
6、浏览器窗口尺寸改变——resize 事件发生时;
回流比重绘的代价要更高,回流的花销跟 render tree 有多少节点需要重新构建有关系,假设你直接操作 body,比如在 body 最前面插入 1 个元素,会导致整个 render tree 回流,这样代价当然会比较高,但如果是指 body 后面插入 1 个元素,则不会影响前面元素的回流。
如果每句 JS 操作都去回流重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作,浏览器会维护 1 个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会 flush 队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。
虽然有了浏览器的优化,但有时候我们写的一些代码可能会强制浏览器提前 flush 队列,这样浏览器的优化可能就起不到作用了。当你请求向浏览器请求一些 style 信息的时候,就会让浏览器 flush 队列,比如:

offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop/Left/Width/Height
clientTop/Left/Width/Height
width,height
请求了 getComputedStyle(), 或者 IE 的 currentStyle

当请求上面的一些属性的时候,浏览器为了给你最精确的值,需要 flush 队列,因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列。
尽量减少回流和重绘
因为回流的开销很大,所以我们在写代码的时候,有很多需要注意的地方:
不要一个一个改变元素的样式属性,最好直接改变 className,但 className 是预先定义好的样式,不是动态的,如果你要动态改变一些样式,则使用 cssText 来改变,如下:
// 不好的写法
var left = 1;
var top = 1;
el.style.left = left + “px”;
el.style.top = top + “px”;

// 比较好的写法
el.className += ” className1″;

// 比较好的写法
el.style.cssText += “; left: ” + left + “px; top: ” + top + “px;”;
让要操作的元素进行 ” 离线处理 ”,处理完后一起更新,这里所谓的 ” 离线处理 ” 即让元素不存在于 render tree 中
a、使用 documentFragment 或 div 等元素进行缓存操作,这个主要用于添加元素的时候,大家应该都用过,就是先把所有要添加到元素添加到 1 个 div(这个 div 也是新加的),最后才把这个 div append 到 body 中。b、先 display:none 隐藏元素,然后对该元素进行所有的操作,最后再显示该元素。因对 display:none 的元素进行操作不会引起回流、重绘。所以只要操作只会有 2 次回流。
不要经常访问会引起浏览器 flush 队列的属性,如果你确实要访问,就先读取到变量中进行缓存,以后用的时候直接读取变量就可以了,见下面代码:

// 别这样写
for(循环) {
elel.style.left = el.offsetLeft + 5 + “px”;
elel.style.top = el.offsetTop + 5 + “px”;
}

// 这样写好点
var left = el.offsetLeft,top = el.offsetTop,s = el.style;
for(循环) {
left += 10;
top += 10;
s.left = left + “px”;
s.top = top + “px”;
}
考虑你的操作会影响到 render tree 中的多少节点以及影响的方式,影响越多,花费肯定就越多。比如现在很多人使用 jquery 的 animate 方法移动元素来展示一些动画效果,想想下面 2 种移动的方法:
// block1 是 position:absolute 定位的元素,它移动会影响到它父元素下的所有子元素。
// 因为在它移动过程中,所有子元素需要判断 block1 的 z -index 是否在自己的上面,
// 如果是在自己的上面, 则需要重绘, 这里不会引起回流
$(“#block1”).animate({left:50});
// block2 是相对定位的元素, 这个影响的元素与 block1 一样,但是因为 block2 非绝对定位
// 而且改变的是 marginLeft 属性,所以这里每次改变不但会影响重绘,
// 还会引起父元素及其下元素的回流
$(“#block2”).animate({marginLeft:50});
参考文章:https://www.cnblogs.com/kevin…https://blog.csdn.net/allenli…https://www.css88.com/archive…

正文完
 0