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

前面有讲到当用户在浏览器输入url以后,通过一系列的过程,会最终向服务器请求到文档数据,文档数据请求到以后,浏览器会将这些数据传给浏览器渲染引擎,渲染引擎开始正式工做了。javascript

构建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会去掉_开头的样式。css


构建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来绘制页面了。html

注意:因为浏览器的流布局,对渲染树的计算一般只须要遍历一次就能够完成。但 table及其内部元素除外,它可能须要屡次计算才能肯定好其在渲染树中节点的属性,一般要花3倍于同等元素的时间。这也是为何咱们要避免使用 table作布局的一个缘由。java


渲染阻塞

在浏览器进行加载时,实际上是并行加载全部资源。对于css和图片等资源,浏览器加载是异步的,并不会影响到后续的加载、html解析和后续渲染。node

css阻塞渲染

由上面过程能够看到,页面布局是在渲染树构建好以后发生的,而渲染树依赖css样式结构体,因此CSS 被视为阻塞渲染的资源但不阻塞html的解析,不会阻塞dom树的构建),这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。jquery

由于css会阻塞渲染,因此咱们应该尽早的尽快地下载到客户端,以便缩短首次渲染的时间。平时在开发的时候,应注意如下几点:chrome

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

同时,还有如下优化点:浏览器

1、媒体查询缓存

经过使用媒体查询,咱们能够根据特定的需求(好比显示或打印),也能够根据动态状况(好比屏幕方向变化、尺寸调整事件等)定制外观,服务器

<link href="style.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 40em)">

看上面的代码,
第一行,这样的普通声明,会阻塞渲染
第二行,这个声明,只在打印网页时应用,所以网页在浏览器中加载时,不会阻塞渲染。
第三行,提供了由浏览器执行的“媒体查询”,只有符合条件时,样式表会生效,浏览器才会阻塞渲染,直至样式表下载并处理完毕。

2、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末尾会先绘制前面的标签

3、动态添加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第三方库,原理同上

4、代码简练


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>

clipboard.png

clipboard.png

经过浏览器的工具上的能够很清楚的看到界面的渲染过程,也能够很清楚的看到请求加载资源的时候,不会对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>

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

clipboard.png

图中能够明显的看出,首先浏览器开始解析html,而后再解析的过程当中遇到css,开始加载css资源,遇到js开始加载js资源,当css加载完成后,开始解析css,js加载完成后,则开始解析js,此时解析html生成dom树会中止,直到js解析完成以后,才再次开始解析html,从新计算样式,布局,生成渲染树,最终才是界面绘制,因此在开发的时候不要将js文件写在头部,这样会影响界面的绘制,致使界面出现空白

浏览器的重绘(repaints)与回流(reflows)

重绘
当render tree中的一些元素须要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,好比background-color。则就叫称为重绘。

回流
当render tree中的一部分(或所有)由于元素的规模尺寸,布局,隐藏等改变而须要从新构建。这就称为回流(reflow)。
每一个页面至少须要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并从新构造这部分渲染树,完成回流后,浏览器会从新绘制受影响的部分到屏幕中,该过程成为重绘。

回流必然会形成重绘,重绘不会形成回流。

回流什么时候发生:

当页面布局和几何属性改变时就须要回流。下述状况会发生浏览器回流:

一、添加或者删除可见的DOM元素;

二、元素位置改变;

三、元素尺寸改变——边距、填充、边框、宽度和高度

四、内容改变——好比文本改变或者图片大小改变而引发的计算值宽度和高度改变;

五、页面渲染初始化;

六、浏览器窗口尺寸改变——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...