整体流程如上图所示,大概可描述为:
- 用户从浏览器里输入请求信息
- 网络进程发起 URL 请求
- 服务器响应 URL 请求之后,浏览器进程开始准备渲染进程
- 渲染进程准备完毕后,向渲染进程提交页面数据(提交文档阶段)
- 渲染进程接受完文档信息后,开始解析页面和加载子资源,完成页面渲染
从输入 URL 到页面展示详细过程
用户输入
用户在地址栏输入关键字后,地址栏会根据关键字来判断是搜索内容
还是请求 URL
。
- 若判断为搜索内容,地址栏使用默认搜索引擎,合成新的带搜索关键字的 URL(q 后为搜索关键字)
https://www.google.com/search?q=google&xxxxxx
- 若判断内容符合 URL 规则,则地址栏会根据输入内容,合称为完整的 URL
www.baidu.com => https://www.baidu.com/
输入关键字后回车,标签上的图标变成 loading 状态,此时页面没有立即替换为目标页面;需要等待提交文档阶段结束,页面内容才会被替换。(见下图)
URL 请求过程
页面请求资源过程,浏览器进程通过 IPC(进程间通信) 把 URL 请求发送至网络进程,网络进程收到后发起真正的 URL 请求。具体过程如下:
- 查找本地有无缓存资源。有,返回给浏览器进程。
- 没有,直接发起网络请求。
- DNS 解析来获取请求域名的服务器 IP 地址。
- 若请求协议时 HTTPS,还需要建立 TLS(安全传输层协议) 连接。
- 用 IP 地址和服务器建立 TCP 连接。(同一域名同时最多只能建立 6 个连接,超过的排队等待)(通过传输层、网络层等加上相应的头)
- 建立连接后,构建请求头、行等信息,将相关 cookie 等数据附加到请求头中,然后发送给服务器
- 服务器接受到请求信息后,根据请求信息生成响应数据,发给网络进程。(响应数据又顺着应用层——传输层——网络层——网络层——传输层——应用层的顺序返回到网络进程)
- 网络进程接收了响应行、头后便开始解析响应内容。
- 若返回的状态时是 301 或 302,则浏览器需要重定向到其他 URL,一切从头开始。
- 浏览器根据响应头中的
Content-Type
字段判断响应体的数据类型;若是application/octet-stream
字节流类型,浏览器会按下载类型来处理,该请求会被提交给浏览器的下载管理器,流程结束。若是 HTML,则进入下个流程:准备渲染进程
准备渲染进程 & 提交文档
不同主域名的页面使用不同的渲染进程,主域名相同的使用同一个渲染进程;
- 渲染进程准备完毕后,
浏览器进程
发出提交文档
的消息。 - 渲染进程收到后,会和网络进程建立传输数据的管道。
- 传输完毕后,渲染进程会返回“确认提交”的消息给浏览器进程。
- 浏览器进程收到确认的消息后,会更新浏览器界面状态(安全状态、URL、前进后退的历史状态)并更新 web 页面。(此时 web 页面是空白页面)
- 导航流程结束,进入渲染阶段。
渲染阶段
按照渲染的时间顺序,流水线可分为: 构建 DOM 树 -> 样式计算 -> 布局 -> 分层 -> 绘制 -> 分块 -> 光栅化 -> 合成等阶段。
每个阶段的流程如下:接收输入的内容 -> 处理 -> 输出内容
构建 DOM 树
接收 html 文件 -> html 解析器解析 -> 输出树状解构的 DOM
样式计算
大体流程:接受 css 文本 -> 计算出 dom 节点的具体样式 -> 输出计算完成后的 DOM 样式
具体步骤如下:
-
接收 css 文本,将其转为浏览器可理解的结构:stylesheets css 三种来源:内联、外部引入、style
-
转换 stylesheets 中属性值,使其标准化
body { font-size: 2em }
p {color:blue;}
div { font-weight: bold}
// 标准化后
body { font-size: 32px }
p {color: rgb(0,0,255);}
div { font-weight: 700}
- 计算出 DOM 树中每个节点的具体样式 计算遵守 css 继承、层叠两个规则,如下图:
所有子节点都继承父节点的样式。计算完后输出每个 DOM 节点的样式(保存在 ComputedStyle 解构内,可在 elements -> computed 标签下查看)
布局
经过上面过程,有了 DOM 树和树中元素的样式,但每个元素的位置还不确定,不足以显示页面。
Chrome 在布局阶段的任务:创建布局树与布局计算。
-
创建布局树 遍历 DOM 中的可见节点,将其添加到布局树中。不可见的节点会被忽略,比如:head 标签下的内容,display 为 none
-
布局计算
计算布局树节点的坐标位置,计算完执行布局时会把运算的结果会重新写入布局树中,所以布局树既是输入内容也是输出内容;此处设计不合理,chrome 正在重构,下一代布局系统 LayoutNG 试图分离输入与输出。
分层
布局完之后不会立即绘制,渲染引擎会为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)
。在开发者工具下的 Layers 标签可以看见页面的分层情况。
-
布局树和图层树的关系
由上图可得,不是每个节点都有图层,若没有,就属于它父节点的图层。 -
什么情况下,渲染引擎会为特定节点创建新图层?
- 拥有层叠上下文的属性会被提升为单独的一层。
层叠上下文
层叠上下文让 HTML 具有三维概念,按照自身属性优先级垂直分布在 z 轴上。 - 需要剪裁(clip)的地方也会被创建为图层。 文字内容比较多,超出显示区域,这时就产生裁剪。渲染引擎会为文字部分单独创建一个层,若出现滚动条,滚动条也会被提升为单独的层。
图层绘制
画图例子: 给你一张纸,画一个背景蓝色,中间为红色的圆,再在圆上画一个绿色三角形,如何画?
- 绘制蓝色背景
- 在中间绘制红色圆
- 在圆上绘制绿色三角形
图层绘制与之类似,将一个图层拆分成很多小的绘图指令
,把指令按顺序组成列表去绘制。(每个元素背景、前景、边框都需要单独的绘图指令)
开发者工具 Layers 标签,选中 document 层,可重现绘制过程,如下。
栅格化(raster)操作
绘制列表只用来记录绘制顺序与指令,真正的绘制操作由渲染引擎中的合成线程来完成。如上图,绘制列表准备好后,主线程会把它提交给合成线程。
有的图层很大,页面要使用滚动条滚好久才能到底部;通过视口,用户只能看到一小部分,这种情况下如果绘制所有图层内容,会产生很大开销且没必要。
合成线程如何处理?
合成线程会将图层划分成图块(tile),大小通常为(256256,512512)。
栅格化(将图块转为位图)
执行。图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有图块栅格化都在线程池内执行。
栅格化过程会使用 GPU 来加速生成,生成的位图被保留在 GPU 内存中。(GPU 操作在 GPU 进程中,栅格化在渲染进程中,这个过程涉及跨进程操作。)
合成与显示
当所有图块都被栅格化,合成进程会生成一个绘制图块的命令DrawQuad
,然后将该命令提交给浏览器进程。
浏览器进程里有个 viz 组件,接收到 DrawQuad 命令后,根据其内容,将页面绘制到内存,最后再将内存显示到屏幕上。
完整渲染流程总结
- 渲染进程将 HTML 内容转换为能够读懂的DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,提交给合成线程。
- 合成线程将图层分成图块,并在栅格化线程池中将图块转化为位图。
- 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
重排、重绘、合成
- 重排
更新元素几何属性,开销最大 - 重绘
更新元素颜色 - 合成
直接合成,避开重排重绘。
Some question
-
同一站点共用一个渲染进程,那假设有2个标签页是同一站点,我在A标签页面写个死循环,导致页面卡死,B页面是否也是卡死了呢?
多个页面公用一个渲染进程,也就意味着多个页面公用同一个主线程,所有页面的任务都是在同一个主线程上执行,这些任务包括渲染流程,JavaScript执行,用户交互的事件的响应等等,但是如果一个标签页里面执行一个死循环,那么意味着该JavaScript代码会一直霸占主线程,这样就导致了其它的页面无法使用该主线程,从而让所有页面都失去响应!
-
若下载 CSS 文件阻塞了,会阻塞 DOM 树的合成吗?会阻塞页面的显示吗?
不会阻塞 DOM 树合成,html 转为 DOM 树的过程中,发现文件请求会交给网络进程去请求文件,渲染进程继续解析 html。 会阻塞页面显示,计算样式时需要 css 文件资源,若资源阻塞,会等待至网络超时,network 报出响应错误,渲染进程继续层叠样式计算。 -
JS 和 CSS 都可能阻塞 DOM 解析
-
当从服务器接收HTML页面的第一批数据时,DOM解析器就开始工作了,在解析过程中,如果遇到了JS脚本,如下所示:
<html> <body> 掘金 <script> document.write("--foo") </script> </body> </html>
那么DOM解析器会先执行JavaScript脚本,执行完成之后,再继续往下解析。
-
那么第二种情况复杂点了,我们内联的脚本替换成js外部文件,如下所示:
<html> <body> 掘金 <script type="text/javascript" src="foo.js"></script> </body> </html>
这种情况下,当解析到JavaScript的时候,会先暂停DOM解析,并下载foo.js文件,下载完成之后执行该段JS文件,然后再继续往下解析DOM。这就是JavaScript文件为什么会阻塞DOM渲染。
-
我们再看第三种情况,还是看下面代码:
<html> <head> <style type="text/css" src = "theme.css" /> </head> <body> <p>掘金</p> <script> let e = document.getElementsByTagName('p')[0] e.style.color = 'blue' </script> </body> </html>
当我在JavaScript中访问了某个元素的样式,那么这时候就需要等待这个样式被下载完成才能继续往下执行,所以在这种情况下,CSS也会阻塞DOM的解析。
-
渲染进程的帧是什么?
可以拿放电影电影来解释,通常,电影的帧速是24,也就是说每秒切换24幅画面,其中的每幅画面就是一帧。理解什么是帧后,我们在回过头看看我们的页面。由于目前大多数设备的屏幕刷新率为 60 次/秒。因此,如果页面中有一个动画、或一个渐变效果、或者用户正在滚动页面,那么浏览器渲染动画的频率至少要和刷新频率保持一致,也就是每秒需要更新60次,这样我们就能计算出来生成每帧的预算只有(1/60)毫秒,也就是16毫秒多一点(1 秒/ 60 = 16.66 毫秒)。如果超过16毫秒,帧率将下降,并且会出现画面抖动现象,此现象通常被称为卡顿,会对用户体验产生负面影响。
所以,如果想要保证画面的流畅,就需要尽量降低每帧的渲染时间,所以局部更新流水线显得非常重要了,能大大减少处理每帧所消耗的时间。