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

前面有讲到当用户在浏览器输入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三者的差异

  1. IE 只要看到HTML 标签就会进行绘制
  2. chrome 不管css放在前面还是后面,都要等到CSSOM构建形成后才会绘制到页面上
  3. 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, [email protected]"/>
    <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>

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

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

通过浏览器的工具上的可以很清楚的看到界面的渲染过程,也可以很清楚的看到请求加载资源的时候,不会对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, [email protected]"/>
    <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>

再次查看浏览器的渲染过程:

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

图中可以明显的看出,首先浏览器开始解析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...

相关推荐