# 渲染流程:HTML、CSS和JavaScript是如何变成页面的?

HTML转换为页面,其实就是HTML、CSS、JavaScript通过中间渲染模块的处理, 最终输出为页面。

HTML、CSS和JavaScript的含义:
HTML、CSS和JavaScript

HTML,超文本标记语言,由标记和文本组成。标记也称为标签,每个标签都有它 的语意,浏览器会根据标签的语意来正确展示HTML内容。

CSS,层叠样式表,由选择器和属性组成,通过CSS可以改变HTML的字体颜色,大小等信息。

JavaScript,一种应用于web端的轻量级,解释型或即时编译型的编程语言。一般会作为 页面脚本,实现页面动态效果。

了解了HTML、CSS、JavaScript后,我们开始分析渲染模块。

渲染模块在执行过程中会被划分为很多子阶段,输入的HTML经过子阶段最后输出 像素,这样的一个处理流程叫渲染流水线

渲染流水线示意图:
渲染流水线

按照渲染的时间顺序,流水线可分为:构建DOM树、样式计算、布局阶段、分层、绘制、 分块、光栅化和合成

# 构建DOM树

由于浏览器无法直接解析HTML,所以需要将HTML转换为浏览器能够理解的结构,DOM树

树结构类似于生活中的“树”,其中每个点称为节点,相连的节点称为父子节点

DOM树构建过程示意图:
DOM树构建过程

构建DOM树的输入内容是一个HTML文件,然后经过HTML解析器解析,最终输出树状结构的DOM。 通过“开发者工具”->“Console”控制台,输出“document”回车后可查看完整的DOM树结构。

可以了解到,DOM和HTML内容几乎是一样的,但是和HTML不同的是,DOM是保持在内存中树状结构, 可以通过JavaScript来查询和修改内容。

而要让DOM节点拥有正确的样式,需要样式计算。

# 样式计算

样式计算的目的是为了计算出DOM节点中每个元素的具体样式,该阶段可分成三步:

# 1、把CSS转换为浏览器能够解析的结构

HTML加载CSS的三种方式:
HTML加载CSS的三种方式

CSS样式主要由三种加载方式:

  • 通过link引用的外部CSS文件
  • style标记内的CSS
  • 元素的style属性内嵌的CSS

由于浏览器也无法解析纯文本的CSS样式,所以当渲染引擎接收到CSS文本时, 会将CSS文本转换为浏览器可以理解的结构,styleSheets

通过控制台document.styleSheets可查看结构:
document.styleSheets

通过渲染引擎将CSS文本转换为浏览器可识别的styleSheets结构数据,并提供了查询 和修改功能。

# 2、标准化样式表中的属性值

CSS文本中存在很多属性值,例如1em、blue、bold等属性值不容易被渲染引擎 识别,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个 过程就是属性值标准化。

标准化属性值:
标准化属性值

# 3、计算DOM数中每个节点的具体样式

样式属性标准化后,徐要计算DOM树中每个节点的样式属性,其中涉及CSS的继承规则 和层叠规则

# CSS继承

CSS继承就是每个DOM节点都包含了父节点的样式。例如:

body { font-size: 20px }p {color:blue;}span {display: none}div {font-weight: bold;color:red}div p {color:green;}

应用到DOM节点效果为:
计算后DOM的样式

可了解到,例如body节点的font-size的属性,body节点下的所以子节点 都继承了。

另可通过“开发者工具”->Element,查看“style”标签:
样式的继承过程

通过分析,我们可以选择对应第一区域对应的元素,可查询改元素的样式(对应区域2中); 并且可通过区域3可查看对应样式的来源心情,其中,UserAgent样式,是浏览器 提供的一组默认样式,如果不修改任何样式,默认使用的是UserAgent样式。

# CSS层叠

层叠是CSS的一个基本特征,它定义了如何合并来自多个源的属性值的算法。 CSS的全称“层叠样式表”即强调了这点

可通过“开发者工具”->Element标签,“Computed”查看最后的计算样式:
DOM元素最终计算样式

样式计算阶段的目的就是为了计算出DOM节点中每个元素的具体样式,在计算过程 中遵守CSS的继承和层叠两个规则。这个阶段最终输出的内容是每个DOM节点的样式, 并被保存在ComputdStyle的结构内。

# 布局阶段

有了DOM树和DOM树中元素的样式,接下来还需要计算出DOM树中可见元素的几何位置, 这个计算过程叫布局

Chrome在布局阶段需完成两个任务:创建布局树和布局计算。

# 1、创建布局树

其中DOM树中还包含很多不可见的元素,例如head标签,display为none属性的元素等。所以在 显示之前,需要额外构建一颗只包含可见元素的布局树

布局树构建过程:
布局树构建过程

为了构建布局树,浏览器需要:

  • 遍历DOM树中所以可见节点,并添加节点到布局中;
  • 忽略不可见的节点,如head标签下的内容,display:none等。

# 2、布局计算

有了完整的布局树后,则需计算布局树节点的坐标位置。而HTML采用的是 流式布局模型,基本原则是按照元素在顺序遍历过程中依次从左到右、从上 到下的排序方式确定位置区域。当然还存在一些特殊的布局方式,例如通过position 属性的定位布局和float实现的浮动布局等。

由于布局的计算过程相当复杂,我们先了解大概的布局方式。

# 阶段总结

我们了解到了渲染流程的前三个阶段为:DOM生成、样式计算和布局

  • 浏览器不能直接解析HTML数据,所以第一步需要将其转换为DOM树结构;
  • 生成DOM树后,接着根据CSS样式表,计算DOM树所以节点的样式;
  • 最后根据计算DOM元素的布局信息,保存在布局树中。

# 分层

因为页面中有很多复杂的效果,如3D变换、页面滚动,z-index等,为了实现 这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一颗对应的 图层树(LayerTree)。其类似于PS的图层概念。

浏览器的页面实际上被分层了很多图层,这些图层叠加后合成最终的页面

布局树和图层树的关系:
布局树和图层树关系示意图

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的 层,那么这个节点就从属于父节点的图层。例如上图的span就从属于父节点图层。

通常满足其下两点中的任意一点即可被提升为单独的一个图层:

第一点,拥有层叠上下文属性的元素会被提升为单独的一层

页面是个二维页面,但是层叠上下文能够让HTML元素具有三维概念,这些HTML元素按照 自身属性的优先级分布在垂直于这个二维页面的z轴上。

层叠上下文示意图:
层叠上下文示意图

例如图中的,定位属性的元素、定义透明度属性的元素、使用CSS滤镜的元素 等都具有层叠上下文属性。

第二点,需要剪裁的地方也会被创建为图层


<style>
      div {
            width: 200;
            height: 200;
            overflow:auto;
            background: gray;
        } 
</style>
<body>
    <div >
        <p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p>
        <p>从上图我们可以看到,document层上有AB层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p>
        <p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p> 
    </div>
</body>

例如所示,文字所显示的区域超过了200*200的显示范围时就产生了剪裁,渲染引擎会把剪裁文字内容 的一部分用于显示在div区域。

出现这种剪裁情况下,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会 被提升为单独的层。

被裁剪的内容所在单独图层的示意图:
被裁剪的内容所在单独图层的示意图

所以,元素有了层叠上下文的属性或者需要被检查,满足任意一点,就会被提升为单独的一层

# 图层绘制

在完成图层树的构建后,渲染引擎会对图层树中的每个图层进行绘制,渲染引擎会把每个图层的 绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。

绘制列表:
绘制列表

绘制列表中的指令其实就是让其执行一个简单的绘制操作,而每个原生的背景、边框等都需要单独 的指令绘制,通过几条绘制指令来实现绘制一个元素。

“开发者工具”->“Layers”可查看“document”层等的绘制列表过程:
一个图层的绘制列表

# 栅格化操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,实际上绘制操作是由渲染引擎 中的合成线程来完成的。

渲染进程中的合成线程和主线程:
渲染进程中的合成线程和主线程

当图层的绘制列表准备完成后,主线程会把该绘制列表提交给合成线程。

通常一个页面很大,而用户有时只能看到其中一部分,所以屏幕上页面的可见区域叫视口(ViewPort)。

这种情况下,要绘制出所有图层内容的话产生的开销太多,所以,合成线程会将图层划分为图块(title), 这些图片的大小通常是256256或512512。

图层图块示意图:
图层划分为图块示意图

合成线程会按照视口附件的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。 所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程 维护了一个栅格化的线程池,所以的图块栅格化都是在线程池内执行的。

合成线程提交图块给栅格化线程池:
合成线程提交图块给栅格化线程池

通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫快速栅格化或者 GPU栅格化,渲染进程会把生成图块的指令发送给GPU进程,然后在 GPU进程中执行生成图块的位图,生成的位图被保存在GPU内存中。

GPU栅格化:
GPU栅格化

# 合成和显示

所有图块都被栅格化,合成线程就会生成一个绘制图块的命令,“DrawQuad”,然后 将该命令提交给浏览器进程。

浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后根据 DrawQuad命令将其页面内容绘制到内存中,最后将内存显示在屏幕上。

# 总结

完整的渲染流水线:
完整的渲染流水线

综上,一个完整的渲染流程为:

1、渲染进程将HTML内容转换为浏览器能识别的DOM树结构。

2、渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出 DOM节点的样式。

3、创建布局树,并计算元素的布局信息。

4、对布局树进行分层,并生成分层树

5、为每个图层生成绘制列表,并将其提交给合成线程。

6、合成线程将图层分成图块,并在栅格化线程池中将图块转换成位图。

7、合成线程发送绘制图块命令DrawQuad给浏览器进程。

8、浏览器进程根据DrawQuad消息生成页面,并显示到显示器上。

# 推荐阅读

HTML标签:https://www.w3school.com.cn/html/html_primary.asp

CSS:https://www.w3school.com.cn/css/index.asp

JavaScript:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript

层叠上下文属性:https://developer.mozilla.org/zh-CN/docs/Web/Guide/CSS/Understanding_z_index/The_stacking_context

图解浏览器的工作原理:https://www.jianshu.com/p/5a52f2492759