浏览器是如何渲染页面的?
当浏览器的网络线程收到
HTML
文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。
整个渲染流程分为多个阶段,分别是:
HTML
解析、样式计算、布局、分层、绘制、分块、光栅化、画每个阶段都有明确的输入输出,上个阶段的输出会成为下个阶段的输入。
这样,整个渲染流程就形成了一套组织严密的生产流水线。
-
解析HTML
解析过程中遇到
CSS
解析CSS
,遇到JS
执行JS
,为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载HTML
中的外部CSS
文件和外部的JS
文件。如果主线程解析到
link
位置,此时外部的CSS
文件还没有下载解析好,主线程不会等待,继续解析后续的HTML
。这是因为下载和解析CSS
的工作实在预解析线程中进行的。这就是CSS
不会阻塞HTML
解析的根本原因。如果主线程解析到
script
位置,会停止解析HTML
,转而等待JS
文件下载好,并将全局代码解析完成后,才能继续解析HTML
。这是因为JS
代码的执行过程可能会修改当前的DOM
树,所以DOM
树的生成必须暂停。这就是JS
会阻塞HTML
解析的根本原因。第一步完成后,会得到
DOM
树和CSSOM
树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在CSSOM
树中。 -
样式计算 - Recalculate Style
主线程会遍历得到的
DOM
树,依次为树中的每个节点计算出它最终的样式,称之为Computed Style。- 在这一过程中的,很多预设值会变成绝对值,比如
red
会变成reg(255,0,0)
; - 相对单位会变成绝对单位,比如
em
会变成px
这一步完成后,会得到一颗带有样式的
DOM
树。-
CSS
属性计算过程首先,不知道你有没有考虑过这样的一个问题,假设在
HTML
中有这么一段代码:<body> <h1>这是一个h1标题</h1> </body>
上面的代码也非常简单,就是在
body
中有一个h1
标题而已,该h1
标题呈现出来的外观是如下:目前我们没有设置该
h1
的任何样式,但是却能看到该h1
有一定的默认样式,例如有默认的字体大小、默认的颜色。那么这个h1
元素上面除了有默认的字体大小、默认颜色等属性以外,究竟还有哪些属性呢?答:**该元素上面会有
CSS
所有的属性。**可以打开浏览器的开发者面板,选择【元素】,切换到【计算样式】,之后勾选【全部显示】,此时就可以看到在此h1
上面所有的CSS
属性对应的值。换句话说,我们所书写的任何一个
HTML
元素,实际上都有完整的一整套CSS
样式。这一点往往是让初学者比较意外的,因为我们平时在书写 CSS 样式时,往往只会书写必要的部分,例如前面的:p{ color:red; }
这往往会给我们造成一种错觉,认为该
p
元素上面就只有color
属性。而真实的情况确是,任何一个HTML
元素,都有一套完整的CSS
样式,只不过你没有书写的样式,大概率可能会使用其默认值。例如上图中h1
一个样式都没有设置,全部都用的默认值。但是注意,这里强调的是“大概率可能”,难道还有我们“没有设置值,但是不使用默认值”的请况么?
答:确实是有的,所以才要强调了解“CSS属性的计算过程”。
总的来讲,属性值的计算过程,分为如下这么4个步骤:
- 确定声明值
- 层叠冲突
- 使用继承
- 使用默认值
-
确定声明值
所谓声明值就是作者自己所书写的
CSS
样式,例如前面的:p{ color:red; }
这里我们声明了
p
元素为红色,那么就会应用此属性设置。当然,除了作者样式表,一般浏览器还会存在“用户代理样式表”,简单来讲就是浏览器内置了一套样式表。
在上面的实例中,作者样式表中设置了
color
属性,而用户代理样式表(浏览器提供的样式表)中设置了诸如display、margin-block-start、margin-block-end、margin-inline-start、margin-inline-end
等属性对应的值。这些值目前来讲也没什么冲突,因此最终就会应用这些属性值。
-
层叠冲突
在确定声明值时,可能出现一种情况,那就是声明的样式规则发生了冲突。
此时会进入解决层叠冲突的流程。而这一步又可以细分为下面这三个步骤:
- 比较源的重要性
- 比较优先级
- 比较次序
比较源的重要性
当不同的
CSS
样式来源拥有相同的声明时,此时就会根据样式表来源的重要性确定应用哪一条样式规则。我们的样式表的源整体来说有三种:
- 浏览器会有一个基本的样式表来给任何网页设置默认样式。这些样式统称为 用户代理样式
- 网页的作者可以定义文档的样式,这是最常见的样式表,称之为 页面作者样式
- 浏览器的用户,可以使用自定义样式表定制使用体验,称之为 用户样式
对应的重要性顺序依次为:页面作者样式>用户样式>用户代理样式
参阅MDN:https://developer.mozilla.org/zh-CN/docs/Web/CSS/Cascade
示例:现在有 页面作者样式表和 用户代理样式表中存在属性的冲突,那么会以作者样式表优先。
p{ color:red; display:inline-block; }
可以明显的看到,作者样式表和用户代理 样式表中同时存在
display
属性的设置,最终作者样式表干掉了用户代理样式表中冲突的属性。这就是第一步,根据不同源的重要性来决定应用哪一个源的样式。比较优先级
那么接下来,如果是在同一源中有样式声明冲突怎么办呢?此时就会进行样式声明的优先级比较。
例如:
<div class="test"> <h1>test</h1> </div>
.test h1{ font-size: 50px; } h1 { font-size: 20px; }
在上面的代码中,同属于 页面作者样式,源的重要性是相同的,此时会以选择器的权重来比较重要性。很显然,上面的选择器的权重要大于下面的选择器,因此最终标题呈现为
50px
可以看出,落败的作者样式在
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
这一条属性值。至此,样式声明中存在冲突的所有情况,就全部被解决了。
-
使用继承
层叠冲突这一步完成后,解决了相同元素被声明了多条样式规则究竟应用那一条样式规则的问题。
那么如果没有声明的属性呢?此时就使用默认值吗?
答:不是的,此时还有第三个步骤,那就是使用继承而来的值
例如:
<div> <p>Lorem ipsum dolor sit amet.</p> </div>
div{ color:red; }
在上面的代码中,我们针对
div
设置了color
属性值为红色,而针对p
元素我们没有声明任何的属性,但是由于color
是可以继承的,因此p
元素从最近的div
身上继承到了color
属性的值。这里要注意两点:
-
首先是强调了 最近的
div
元素,例子:<div class="test"> <div> <p>Lorem ipsum dolor sit amet.</p> </div> </div>
div { color: red; } .test{ color: blue; }
因为这里并不涉及到选中
p
元素声明color
值,而是从父元素上面继承到color
对应的值,因此这里是 就近原则,谁近就听谁的。初学者往往会产生混淆,又去比较权重,但是这里根本不会涉及到权重比较,因为压根就没有选中到p
元素。 -
哪些属性能够继承?
关于这一点,可以在MDN上面查阅到。例如
text-align
:以下是可以继承的常见的 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。
-
-
-
使用默认值
好了,目前走到这一步,如果属性值都还不确定下来,那么就只能是使用默认值了。
如下图所示:
前面我们也说过,一个
HTML
元素要在浏览器中渲染出来,必须具备所有的CSS
属性值,但是绝大部分我们是不会去设置的,用户代理样式表里面也不会去设置,也无法从继承拿到,因此最终都是用默认值。
总结:页面作者样式>用户样式>用户代理样式>继承>默认样式
-
一道面试题
下面的代码,最终渲染出来的效果,
a
元素是什么颜色?p
元素是什么颜色?<div> <a href="">test</a> <p>test</p> </div>
div{ color:red; }
解答如下:
因为
a
元素在用户代理样式表中已经设置了color
属性的值,因此会应用此声明值。而在p
元素中无论是作者样式表还是用户代理 样式表,都没有对此属性进行声明,然而由于color
属性是可以继承的,因此最终p
元素的color
属性值通过继承来自于父元素。
- 在这一过程中的,很多预设值会变成绝对值,比如
-
布局-Layout
布局阶段会依次遍历
DOM
树的每一个节点,计算每一个节点的几何信息。例如节点的宽高、相对包含块的位置。大部分时候,
DOM
树和布局树并非一一对应。比如
display:none
的节点没有任何几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然DOM
树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中、还有匿名行盒、匿名块盒等等都会导致DOM
树和布局树无法一一对应。-
你不知道的
CSS
之包含块一说到
CSS
盒模型,这是很多小伙伴耳熟能详的知识,甚至有的小伙伴还能说出border-box
和content-box
这两种盒模型的区别。在content-box
模式下,元素的宽高只包含元素的内容,不包含边框和填充。而在border-box
模式下,元素的宽高包含元素的内容、边框和填充。但是一说到
CSS
包含块,有的小伙伴就懵圈了,什么是包含块?包含块英语全称为 containing block,实际上平时你在书写
CSS
时,大多数情况下你是感觉不到它的存在,因此你不知道这个知识点也是一件很正常的事情。但是这玩意是确确实实存在的,在CSS
规范中也是明确书写了的:https://drafts.csswg.org/css2/#containing-block-details并且,如果你不了解它的运行机制,有时就会出现一些你认为的莫名其妙的现象。
那么,这个包含块究竟说了什么内容呢?
简单的说:就是元素的尺寸和位置,会受它的包含块所影响。对于一些属性,例如
width,height,padding,margin
,绝对定位元素的偏移值(比如position
被设置为absolute
或fixed
),当我们对其赋予百分比值时,这些值的计算值,就是通过元素的包含块计算得来。例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
这个盒子的宽高是多少?相信你能够很自信的回答这个简单的问题,
div.item
盒子的width
为250px,height
为250px。这个答案确实是没有问题的,但是如果我追问你是怎么得到这个答案的,我猜不了解包含块的你大概率会说,因为他的父元素
div.container
的width
为500px,50%就是250px,height
为300px,因此50%就是150px。这个答案实际上是不准确的。正确的答案应该是,
div.item
的宽高是根据它的包含块来计算的,而这里包含块的大小,正是这个元素最近的祖先块的内容区。因此正如我前面所说,很多时候你都感觉不到包含块的存在。
-
包含块分为两种,一种是根元素(
HTML
元素)所在的包含块,被称为初始包含块(initial containing block)。对于浏览器而言,初始包含块的大小等于视口viewport
的大小,基于在画布的原点(视口左上角)。他作为元素绝对定位和固定定位的参照物。 -
另一种是对于非根元素,对于非根元素的包含块判定就有几种不同的情况了。大致可以分为如下几种:
- 如果元素的
position
是relative
或static
,那么包含块由离他最近的块容器(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; }
正确答案:
怎么样?有没有和你想象的对上?
其实原因也非常简单,根据上面的第三条规则,对于
div.item2
来讲,他的包含块应该是div.container
,而非div.item
。实际上对于非根元素来讲,包含块还有一种可能,那就是如果
position
属性是absolute
或fixed
,包含块也可能是由满足以下的最近父级元素的内边距的边缘组成的:transform
或perspective
的值不是none
will-change
的值时transform
或perspective
filter
的值不是none
或will-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),此时的渲染效果却发生了改变,如下图所示:
可以看到,此时对于
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
代码的情况下,你能说出各自的包含块么?对应的结果如下:
元素 包含块 html initial C.B. (UA-dependent) body html div1 body p1 div1 p2 div1 em1 p2 strong1 p2 其他的都好理解,不过
strong1
比较例外,它的包含块确实p2
,而非em1
。为什么会这样?- 如果元素的
position
是relative
或static
,那么包含块由离他最近的块容器(block container)的内容区域(content area)的边缘建立。
没错,因为
em1
不是块容器,而包含块是 离他最近的块容器的内容区域,所以是p2
.接下来添加如下的
CSS
:#div1{ position:absolute; left: 50px; top: 50px; }
上面的代码我们对
div1
进行了定位,那么此时的包含块会发生变化吗?元素 包含块 html initial C.B. (UA-dependent) body html div1 initial C.B. (UA-dependent) p1 div1 p2 div1 em1 p2 strong1 p2 可以看出,这里
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
。如下表所示:
元素 包含块 html initial C.B. (UA-dependent) body html div1 initial C.B. (UA-dependent) p1 div1 p2 div1 em1 div1(因为定位了,参阅非根元素包含块确定规则的第三条) strong1 em1(因为 em1 变为了块容器,参阅非根元素包含块确定规则的第一条) 另外,关于包含块的知识,在MDN上除了解说了什么是包含块以外,也举出了很多简单易懂的示例。
详见:https://developer.mozilla.org/zh-CN/docs/Web/CSS/Containing_block
-
-
-
分层 - Layer
主线程会使用一套复杂的策略对整个布局树中进行分层。
分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。
滚动条、堆叠上下文、transform、opacity等样式都会或多或少的影响分层结果,也可以通过
will-change
属性更大程度的影响分层结果。 -
绘制 - Paint
主线程会为每一层单独产生绘制指令集,用于描述这一层的内容该如何画出来。
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
-
分块 - Tiling
合成线程首先对每个图层进行分块,将其划分为更多的小区域。
他会从线程池中拿取多个线程来完成分块工作。
-
光栅化 - Raster
合成线程会将块信息交给GPU进程,以极高的速度完成光栅化。
GPU进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。
光栅化的结果,就是一块一块的位图。
-
画 - Draw
合成线程拿到每个层、每个块的位图后,生成一个个【指引(quad)】信息。
指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。
变形发生在合成线程,与渲染主线程无关,这就是
transform
效率高的本质原因。合成线程会把
quad
提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕成像。
常见的面试题:
-
什么是reflow?
reflow 的本质就是重新计算 layout 树。
当进行了会影响布局树的操作后,需要重新计算布局树,会引发 layout。
为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当
JS
代码全部完成后再进行统一计算。所以,改动属性造成的reflow是异步完成的。也同样因为如此,当
JS
获取布局属性时,就可能造成无法获取到最新的布局信息。浏览器在反复权衡下,最终决定获取属性立即reflow。
-
什么是repaint?
repaint的本质就是重新根据分层信息计算了绘制指令。
当改动了可见样式后,就需要重新计算,会引发repaint。
由于元素的布局信息也属于可见样式,所以reflow一定会引起repaint。
-
为什么
transfrom
的效率高?因为
transfrom
既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个【画 - draw】阶段由于draw阶段在合成线程中,所以
transform
的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响transform
的变化。