浏览器之渲染原理

浏览器是如何渲染页面的?

当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。

在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。

整个渲染流程分为多个阶段,分别是:HTML解析、样式计算、布局、分层、绘制、分块、光栅化、画

每个阶段都有明确的输入输出,上个阶段的输出会成为下个阶段的输入。

这样,整个渲染流程就形成了一套组织严密的生产流水线。

image-20230608161422832

image-20230608161449387

  1. 解析HTML

    image-20230608163924529

    解析过程中遇到CSS解析CSS,遇到JS执行JS,为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载HTML中的外部CSS文件和外部的JS文件。

    如果主线程解析到link位置,此时外部的CSS文件还没有下载解析好,主线程不会等待,继续解析后续的HTML。这是因为下载和解析CSS的工作实在预解析线程中进行的。这就是CSS不会阻塞HTML解析的根本原因。

    image-20230608163844815

    image-20230608163659230

    如果主线程解析到script位置,会停止解析HTML,转而等待JS文件下载好,并将全局代码解析完成后,才能继续解析HTML。这是因为JS代码的执行过程可能会修改当前的DOM树,所以DOM树的生成必须暂停。这就是JS会阻塞HTML解析的根本原因。

    image-20230608163718484

    第一步完成后,会得到DOM树和CSSOM树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在CSSOM树中。

    image-20230608165256606

  2. 样式计算 - Recalculate Style

    主线程会遍历得到的DOM树,依次为树中的每个节点计算出它最终的样式,称之为Computed Style。

    • 在这一过程中的,很多预设值会变成绝对值,比如red会变成reg(255,0,0)
    • 相对单位会变成绝对单位,比如em会变成px

    这一步完成后,会得到一颗带有样式的DOM树。

    image-20230608170238613

    • CSS属性计算过程

      首先,不知道你有没有考虑过这样的一个问题,假设在HTML中有这么一段代码:

      <body>
        <h1>这是一个h1标题</h1>
      </body>
      

      上面的代码也非常简单,就是在body中有一个h1标题而已,该h1标题呈现出来的外观是如下:

      image-20220813140724136

      目前我们没有设置该h1的任何样式,但是却能看到该h1有一定的默认样式,例如有默认的字体大小、默认的颜色。那么这个h1元素上面除了有默认的字体大小、默认颜色等属性以外,究竟还有哪些属性呢?

      答:**该元素上面会有CSS所有的属性。**可以打开浏览器的开发者面板,选择【元素】,切换到【计算样式】,之后勾选【全部显示】,此时就可以看到在此h1上面所有的CSS属性对应的值。

      image-20230608172656332

      换句话说,我们所书写的任何一个HTML元素,实际上都有完整的一整套CSS样式。这一点往往是让初学者比较意外的,因为我们平时在书写 CSS 样式时,往往只会书写必要的部分,例如前面的:

      p{
      	color:red;
      }
      

      这往往会给我们造成一种错觉,认为该p元素上面就只有color属性。而真实的情况确是,任何一个HTML元素,都有一套完整的CSS样式,只不过你没有书写的样式,大概率可能会使用其默认值。例如上图中h1一个样式都没有设置,全部都用的默认值。

      但是注意,这里强调的是“大概率可能”,难道还有我们“没有设置值,但是不使用默认值”的请况么?

      答:确实是有的,所以才要强调了解“CSS属性的计算过程”

      总的来讲,属性值的计算过程,分为如下这么4个步骤:

      • 确定声明值
      • 层叠冲突
      • 使用继承
      • 使用默认值
      1. 确定声明值

        所谓声明值就是作者自己所书写的CSS样式,例如前面的:

        p{
        	color:red;
        }
        

        这里我们声明了p元素为红色,那么就会应用此属性设置。

        当然,除了作者样式表,一般浏览器还会存在“用户代理样式表”,简单来讲就是浏览器内置了一套样式表。

        image-20230609091049191

        在上面的实例中,作者样式表中设置了color属性,而用户代理样式表(浏览器提供的样式表)中设置了诸如display、margin-block-start、margin-block-end、margin-inline-start、margin-inline-end等属性对应的值。

        这些值目前来讲也没什么冲突,因此最终就会应用这些属性值。

      2. 层叠冲突

        在确定声明值时,可能出现一种情况,那就是声明的样式规则发生了冲突。

        此时会进入解决层叠冲突的流程。而这一步又可以细分为下面这三个步骤:

        • 比较源的重要性
        • 比较优先级
        • 比较次序
        比较源的重要性

        当不同的CSS样式来源拥有相同的声明时,此时就会根据样式表来源的重要性确定应用哪一条样式规则。

        我们的样式表的源整体来说有三种:

        • 浏览器会有一个基本的样式表来给任何网页设置默认样式。这些样式统称为 用户代理样式
        • 网页的作者可以定义文档的样式,这是最常见的样式表,称之为 页面作者样式
        • 浏览器的用户,可以使用自定义样式表定制使用体验,称之为 用户样式

        对应的重要性顺序依次为:页面作者样式>用户样式>用户代理样式

        参阅MDN:https://developer.mozilla.org/zh-CN/docs/Web/CSS/Cascade

        示例:现在有 页面作者样式表用户代理样式表中存在属性的冲突,那么会以作者样式表优先。

        p{
        	color:red;
        	display:inline-block;
        }
        

        image-20230609093321316

        可以明显的看到,作者样式表和用户代理 样式表中同时存在display属性的设置,最终作者样式表干掉了用户代理样式表中冲突的属性。这就是第一步,根据不同源的重要性来决定应用哪一个源的样式。

        比较优先级

        那么接下来,如果是在同一源中有样式声明冲突怎么办呢?此时就会进行样式声明的优先级比较。

        例如:

        <div class="test">
          <h1>test</h1>
        </div>
        
        .test h1{
          font-size: 50px;
        }
        
        h1 {
          font-size: 20px;
        }
        

        在上面的代码中,同属于 页面作者样式,源的重要性是相同的,此时会以选择器的权重来比较重要性。很显然,上面的选择器的权重要大于下面的选择器,因此最终标题呈现为50px

        image-20230609094758104

        可以看出,落败的作者样式在elements>styles中会被划掉。

        有关选择器的权重问题,简要来说:

        • 关键字:!improtant 设置当前优先级最高,覆盖ID选择器

        • 样式优先级:行内样式>页内样式>外部样式

        • 选择器优先级:id选择器>类选择器>标签选择器

          同类优先级就近原则

        参阅MDN:https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity

        比较次序

        经历了上面的两个步骤,大多数的样式声明能够被确定下来。但是还剩下最后一种情况,那就是样式声明既是同源,权重也相同。

        此时就会进入第三个步骤,比较样式声明的次序

        例如:

        h1{
        	font-size:50px;
        }
        
        h1{
        	fon-size:20px;
        }
        

        在上面的代码中,同样都是 页面作者样式,选择器的权重也相同,此时位于下面的样式声明会层叠掉上面的那一条样式声明,最终会应用20px这一条属性值。

        image-20230609100812613

        至此,样式声明中存在冲突的所有情况,就全部被解决了。

      3. 使用继承

        层叠冲突这一步完成后,解决了相同元素被声明了多条样式规则究竟应用那一条样式规则的问题。

        那么如果没有声明的属性呢?此时就使用默认值吗?

        答:不是的,此时还有第三个步骤,那就是使用继承而来的值

        例如:

        <div>
          <p>Lorem ipsum dolor sit amet.</p>
        </div>
        
        div{
        	color:red;
        }
        

        在上面的代码中,我们针对div设置了color属性值为红色,而针对p元素我们没有声明任何的属性,但是由于color是可以继承的,因此p元素从最近的div身上继承到了color属性的值。

        image-20230609101554117

        这里要注意两点:

        1. 首先是强调了 最近的div元素,例子:

          <div class="test">
            <div>
              <p>Lorem ipsum dolor sit amet.</p>
            </div>
          </div>
          
          div {
            color: red;
          }
          .test{
            color: blue;
          }
          

          image-20230609101826243

          因为这里并不涉及到选中p元素声明color值,而是从父元素上面继承到color对应的值,因此这里是 就近原则,谁近就听谁的。初学者往往会产生混淆,又去比较权重,但是这里根本不会涉及到权重比较,因为压根就没有选中到p元素。

        2. 哪些属性能够继承?

          关于这一点,可以在MDN上面查阅到。例如text-align

          image-20230609102641453

          以下是可以继承的常见的 CSS 属性:

          • 字体系列属性:font、font-family、font-weight、font-size、font-style;

          • 文本系列属性:内联元素:color、line-height、word-spacing、letter-spacing、text-transform;块级元素:text-indent、text-align;

          • 元素可见性:visibility、opacity2、光标属性:cursor;

          • 背景系列属性:background-color、background-image、background-repeat、background-attachment、background-position;

          • 边框系列属性:border-width、border-style、border-color;

          • 列表系列属性:list-style-type、list-style-position、list-style-image;

          • 表格系列属性:table-layout、border-collapse;

          • 文本对齐方式:text-align、vertical-align。

      4. 使用默认值

        好了,目前走到这一步,如果属性值都还不确定下来,那么就只能是使用默认值了。

        如下图所示:

        image-20230609103240194

        前面我们也说过,一个HTML元素要在浏览器中渲染出来,必须具备所有的CSS属性值,但是绝大部分我们是不会去设置的,用户代理样式表里面也不会去设置,也无法从继承拿到,因此最终都是用默认值。

      总结:页面作者样式>用户样式>用户代理样式>继承>默认样式

    • 一道面试题

      下面的代码,最终渲染出来的效果,a元素是什么颜色?p元素是什么颜色?

      <div>
        <a href="">test</a>
        <p>test</p>
      </div>
      
      div{
      	color:red;
      }
      

      解答如下:

      image-20230609104123406

      因为a元素在用户代理样式表中已经设置了color属性的值,因此会应用此声明值。而在p元素中无论是作者样式表还是用户代理 样式表,都没有对此属性进行声明,然而由于color属性是可以继承的,因此最终p元素的color属性值通过继承来自于父元素。

  3. 布局-Layout

    image-20230609105936134

    布局阶段会依次遍历DOM树的每一个节点,计算每一个节点的几何信息。例如节点的宽高、相对包含块的位置。

    大部分时候,DOM树和布局树并非一一对应。

    比如display:none的节点没有任何几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然DOM树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中、还有匿名行盒、匿名块盒等等都会导致DOM树和布局树无法一一对应。

    image-20230609110000884

    image-20230609110020919

    image-20230609110054530

    • 你不知道的CSS之包含块

      一说到CSS盒模型,这是很多小伙伴耳熟能详的知识,甚至有的小伙伴还能说出border-boxcontent-box这两种盒模型的区别。在content-box模式下,元素的宽高只包含元素的内容,不包含边框和填充。而在border-box模式下,元素的宽高包含元素的内容、边框和填充。

      但是一说到CSS包含块,有的小伙伴就懵圈了,什么是包含块?

      包含块英语全称为 containing block,实际上平时你在书写CSS时,大多数情况下你是感觉不到它的存在,因此你不知道这个知识点也是一件很正常的事情。但是这玩意是确确实实存在的,在CSS规范中也是明确书写了的:https://drafts.csswg.org/css2/#containing-block-details

      image-20230609151129206

      并且,如果你不了解它的运行机制,有时就会出现一些你认为的莫名其妙的现象。

      那么,这个包含块究竟说了什么内容呢?

      简单的说:就是元素的尺寸和位置,会受它的包含块所影响。对于一些属性,例如width,height,padding,margin,绝对定位元素的偏移值(比如position被设置为absolutefixed),当我们对其赋予百分比值时,这些值的计算值,就是通过元素的包含块计算得来。

      例1:

      <body>
        <div class="container">
          <div class="item"></div>
        </div>
      </body>
      
      .container{
      	width:500px;
      	height:300px;
      	background-color:skyblue;
      }
      .item{
      	width:50%;
      	height:50%;
      	background-color:red;
      }
      

      仔细阅读上面的代码,然后你认为div.item这个盒子的宽高是多少?

      image-20230609153626858

      相信你能够很自信的回答这个简单的问题,div.item盒子的width为250px,height为250px。

      这个答案确实是没有问题的,但是如果我追问你是怎么得到这个答案的,我猜不了解包含块的你大概率会说,因为他的父元素div.containerwidth为500px,50%就是250px,height为300px,因此50%就是150px。

      这个答案实际上是不准确的。正确的答案应该是,div.item的宽高是根据它的包含块来计算的,而这里包含块的大小,正是这个元素最近的祖先块的内容区。

      因此正如我前面所说,很多时候你都感觉不到包含块的存在。

      1. 包含块分为两种,一种是根元素(HTML元素)所在的包含块,被称为初始包含块(initial containing block)。对于浏览器而言,初始包含块的大小等于视口viewport的大小,基于在画布的原点(视口左上角)。他作为元素绝对定位和固定定位的参照物。

      2. 另一种是对于非根元素,对于非根元素的包含块判定就有几种不同的情况了。大致可以分为如下几种:

        • 如果元素的positionrelativestatic,那么包含块由离他最近的块容器(block container)的内容区域(content area)的边缘建立。
        • 如果position属性是fixed,那么包含块由视口建立。
        • 如果元素使用了absolute定位,则包含块由它的最近的position的值不是static(也就是值为fixed、absolute、relative或sticky)的祖先元素的内边距区的边缘组成。

        前面两条实际上都还比较好理解,第三条往往是初学者容易比较忽视的,我们来看一个示例:

        <body>
            <div class="container">
              <div class="item">
                <div class="item2"></div>
              </div>
            </div>
          </body>
        
        .container {
            width: 500px;
            height: 300px;
            background-color: skyblue;
            position: relative;
        }
        .item {
            width: 300px;
            height: 150px;
            border: 5px solid;
            margin-left: 100px;
        }
        .item2 {
            width: 100px;
            height: 100px;
            background-color: red;
            position: absolute;
            left: 10px;
            top: 10px;
        }
        

        正确答案:

        image-20230609174259607

        怎么样?有没有和你想象的对上?

        其实原因也非常简单,根据上面的第三条规则,对于div.item2来讲,他的包含块应该是div.container,而非div.item

        实际上对于非根元素来讲,包含块还有一种可能,那就是如果position属性是absolutefixed,包含块也可能是由满足以下的最近父级元素的内边距的边缘组成的:

        • transformperspective的值不是none
        • will-change的值时transformperspective
        • filter的值不是nonewill-change的值时filter(只在Firefox下生效)
        • contain的值是paint(例如:contain:paint

        示例:

        <body>
          <div class="container">
            <div class="item">
              <div class="item2"></div>
            </div>
          </div>
        </body>
        
        .container {
            width: 500px;
            height: 300px;
            background-color: skyblue;
            position: relative;
        }
        .item {
            width: 300px;
            height: 150px;
            border: 5px solid;
            margin-left: 100px;
            transform: rotate(0deg); /* 旋转 */
        }
        .item2 {
            width: 100px;
            height: 100px;
            background-color: red;
            position: absolute;
            left: 10px;
            top: 10px;
        }
        

        我们对于上面的代码只新增了一条声明,那就是 transform: rotate(0deg),此时的渲染效果却发生了改变,如下图所示:

        image-20230609175807404

        可以看到,此时对于div.item2来讲,包含块就变成了div.item.

      我们再把CSS规范中所举的例子来看一下。

      <html>
        <head>
          <title>Illustration of containing blocks</title>
        </head>
        <body id="body">
          <div id="div1">
            <p id="p1">This is text in the first paragraph...</p>
            <p id="p2">
              This is text
              <em id="em1">
                in the
                <strong id="strong1">second</strong>
                paragraph.
              </em>
            </p>
          </div>
        </body>
      </html>
      

      上面是一段简单的HTML代码,在没有添加任何CSS代码的情况下,你能说出各自的包含块么?

      对应的结果如下:

      元素包含块
      htmlinitial C.B. (UA-dependent)
      bodyhtml
      div1body
      p1div1
      p2div1
      em1p2
      strong1p2

      其他的都好理解,不过strong1比较例外,它的包含块确实p2,而非em1。为什么会这样?

      • 如果元素的positionrelativestatic,那么包含块由离他最近的块容器(block container)的内容区域(content area)的边缘建立。

      没错,因为em1不是块容器,而包含块是 离他最近的块容器的内容区域,所以是p2.

      接下来添加如下的CSS

      #div1{
      	position:absolute;
      	left: 50px;
      	top: 50px;
      }
      

      上面的代码我们对div1进行了定位,那么此时的包含块会发生变化吗?

      元素包含块
      htmlinitial C.B. (UA-dependent)
      bodyhtml
      div1initial C.B. (UA-dependent)
      p1div1
      p2div1
      em1p2
      strong1p2

      可以看出,这里div1的包含块就发生了变化,变成了初始包含块。这里你可以参考前文中的这两句话:

      • 初始包含块(initial containing block),对于浏览器而言,初始包含块的大小等于视口viewport的大小,基点在画布的原点(视口左上角)。他是作为元素绝对定位和固定定位的参照物。
      • 如果元素使用了absolute定位,则包含块由他的最近的position的值不是static(也就是值为fixed、absolute、relative或sticky)的祖先元素的内边距的边缘组成。

      是不是一下子就理解了。没错,因为我们对div1进行了定位,因此他会应用非根元素包含块计算规则的第三条规则,寻找离他最近的position的值不是static的祖先元素,不过显然body的定位方式为static,因此div1的包含块最终就变成了初始包含块。

      接下来我们继续修改我们的CSS

      #div1 { 
        position: absolute; 
        left: 50px; 
        top: 50px 
      }
      #em1  { 
        position: absolute; 
        left: 100px; 
        top: 100px 
      }
      

      这里我们对em1同样进行了absolute绝对定位,你想想会有什么样的变化?

      没错,em1的包含块不再是p2,而变成了div1,而strong1的包含块也不再是p2了,而是变成了em1

      如下表所示:

      元素包含块
      htmlinitial C.B. (UA-dependent)
      bodyhtml
      div1initial C.B. (UA-dependent)
      p1div1
      p2div1
      em1div1(因为定位了,参阅非根元素包含块确定规则的第三条)
      strong1em1(因为 em1 变为了块容器,参阅非根元素包含块确定规则的第一条)

      另外,关于包含块的知识,在MDN上除了解说了什么是包含块以外,也举出了很多简单易懂的示例。

      详见:https://developer.mozilla.org/zh-CN/docs/Web/CSS/Containing_block

  4. 分层 - Layer

    主线程会使用一套复杂的策略对整个布局树中进行分层。

    分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。

    滚动条、堆叠上下文、transform、opacity等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。

    image-20230609194535978

  5. 绘制 - Paint

    主线程会为每一层单独产生绘制指令集,用于描述这一层的内容该如何画出来。

    image-20230609195203111

    完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。

    image-20230609195229426

  6. 分块 - Tiling

    合成线程首先对每个图层进行分块,将其划分为更多的小区域。

    image-20230609195351221

    他会从线程池中拿取多个线程来完成分块工作。

    image-20230609195408634

  7. 光栅化 - Raster

    合成线程会将块信息交给GPU进程,以极高的速度完成光栅化。

    image-20230609195820656

    GPU进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。

    光栅化的结果,就是一块一块的位图。

    image-20230609195806353

  8. 画 - Draw

    合成线程拿到每个层、每个块的位图后,生成一个个【指引(quad)】信息。

    指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。

    变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。

    合成线程会把quad提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕成像。

    image-20230610174041487

    image-20230610174104191

常见的面试题:

  1. 什么是reflow?

    reflow 的本质就是重新计算 layout 树。

    当进行了会影响布局树的操作后,需要重新计算布局树,会引发 layout。

    为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当JS代码全部完成后再进行统一计算。所以,改动属性造成的reflow是异步完成的。

    也同样因为如此,当JS获取布局属性时,就可能造成无法获取到最新的布局信息。

    浏览器在反复权衡下,最终决定获取属性立即reflow。

    image-20230610175010382

  2. 什么是repaint?

    repaint的本质就是重新根据分层信息计算了绘制指令。

    当改动了可见样式后,就需要重新计算,会引发repaint。

    由于元素的布局信息也属于可见样式,所以reflow一定会引起repaint。

    image-20230610175408237

  3. 为什么transfrom的效率高?

    因为transfrom既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个【画 - draw】阶段

    由于draw阶段在合成线程中,所以transform的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响transform的变化。

    image-20230610180026014

    image-20230610175950123

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yige001

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值