这是一个老生常谈的问题,在面试中也频繁被问题,除了需要掌握在面试中能够简略概括,说出大概内容,我们也需要去时常回顾这中间的详细过程。在这篇文章中你将会学到以下内容:
- 导航流程:从输入URL到页面展示
- 渲染流程:HTML、CSS、JavaScript如何渲染为页面
当然,如果文中提到的内容存在错误,欢迎大家在评论区提出,共同进步~
导航流程
1、用户输入
输入的是URL还是搜索关键字?
浏览器会对URL进行解析,如果解析出来的结果符合URL规则,会将用户输入的内容加上协议,合成完整的URL.
当协议或主机名不合法时,浏览器会将地址栏中输入的文字传给默认的搜索引擎。大部分情况下,在把文字传递给搜索引擎的时候,URL会带有特定的一串字符,用来告诉搜索引擎这次搜索来自这个特定浏览器。
按下回车
当用户输入关键字并键入回车之后,这意味着当前页面即将要被替换成新的页面,但在这个替换流程之前,浏览器存在beforeunload事件,beforeunload 事件允许页面在退出之前执行一些数据清理操作,还可以询问用户是否要离开当前页面。
开始导航
当前页面没有监听 beforeunload 事件或者同意了继续后续流程,那么浏览器便进入下图的状态:
2、URL 请求过程
浏览器缓存
在标签页上的图标进入了加载状态时,会进行页面资源请求过程,浏览器进程会通过IPC管道将URL请求发送至网络进程。首先网络进程会查看本地缓存是否缓存了该资源,如果有缓存直接返回资源给浏览器进程。
这里可以延展出浏览器缓存——强缓存和协商缓存
DNS解析
然后会进行DNS解析,这个过程的具体流程我用一张思维导图表示,如下图:
TLS握手
现在我们成功获取到了IP地址,这时候需要判断下是HTTP协议还是HTTPS协议,如果是HTTPS协议还需要建立TLS连接。
TCP连接
接下来,利用 IP 地址和服务器建立 TCP 连接,也就是我们熟知的三次握手。
注意:Chrome 有个机制,同一个域名同时最多只能建立 6 个TCP 连接,如果在同一个域名下同时有 10 个请求发生,那么其中 4 个请求会进入排队等待状态,直至进行中的请求完成。如果当前请求数量少于6个,会直接建立TCP连接。
为什么两次握手不可以呢?
为了防止已经失效的连接请求报文段突然又传送到了 B,因而产生错误。比如下面这种情况:A 发出的第一个连接请求报文段并没有丢失,而是在网路结点长时间滞留了,以致于延误到连接释放以后的某个时间段才到达 B。本来这是一个早已失效的报文段。但是 B 收到此失效的链接请求报文段后,就误认为 A 又发出一次新的连接请求。于是就向 A 发出确认报文段,同意建立连接。
对于上面这种情况,如果不进行第三次握手,B 发出确认后就认为新的运输连接已经建立了,并一直等待 A 发来数据。B 的许多资源就这样白白浪费了。
如果采用了三次握手,由于 A 实际上并没有发出建立连接请求,所以不会理睬 B 的确认,也不会向 B 发送数据。B 由于收不到确认,就知道 A 并没有要求建立连接。
为什么不需要四次握手?
有人可能会说 A 发出第三次握手的信息后在没有接收到 B 的请求就已经进入了连接状态,那如果 A 的这个确认包丢失或者滞留了怎么办?
我们需要明白一点,完全可靠的通信协议是不存在的。在经过三次握手之后,客户端和服务端已经可以确认之前的通信状况,都收到了确认信息。所以即便再增加握手次数也不能保证后面的通信完全可靠,所以是没有必要的。
连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。
服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。
解析响应头
1)重定向
如果返回的状态码是301/302,说明需要重定向到其他网页。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。
如果是状态码是200,那么浏览器可以继续处理该请求。
2)响应数据类型处理
响应的 Content-Type 标头指明了数据类型。
如果响应是一个 HTML 文件,那么下一步是将数据传递给渲染程序。但如果是 zip 文件或其他文件,则表示它是一个下载请求, 他们需要将数据传递给内容下载管理器。
这也是SafeBrowsing检查的地方。 如果域名和响应数据似乎与某个已知的恶意网站匹配,那么此网络线程 显示警告页面。此外, Ross Origin Read Blocking (CORB) 以便确保跨网站 数据不会进入渲染器进程。
3、准备渲染进程
默认情况下,Chrome 会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,也有一些例外,在某些情况下,浏览器会让多个页面直接运行在同一个渲染进程中。这在之前的文章中提到过,感兴趣的可以看一看多进程架构下,为什么会存在单个页面崩溃导致所有页面崩溃?
总结来说,打开一个新页面采用的渲染进程策略就是:
- 通常情况下,打开新的页面都会使用单独的渲染进程;
- 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。
4、提交导航
所谓提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:
- 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
- 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
- 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
- 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
其中,当渲染进程确认提交之后,更新内容如下图所示:
5、渲染阶段
文档提交后,我们就进入了渲染阶段,渲染进程的核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之互动的网页。他的具体步骤如下图,接下来我们将会对每一个步骤进行详细介绍,为什么会存在这个步骤以及该步骤的主要流程是什么。
构建 DOM 树
为什么要构建 DOM 树呢?这是因为浏览器无法直接理解和使用 HTML ,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。整个 DOM 和 HTML 文档几乎是一对一的关系。
子资源加载
网站通常会使用图片、CSS 和 JavaScript 等外部资源。这些文件需要从网络或缓存加载。主线程在解析以构建 DOM 时可以在找到它们时逐个请求,但为了加快速度,会并发运行“预加载扫描器”。如果 HTML 文档中有 <img>
或 <link>
之类的内容,预加载扫描程序会查看 HTML 解析器生成的令牌,并将请求发送到浏览器进程中的网络线程。
JavaScript 可能会阻止解析
当 HTML 解析器找到 <script>
标记时,它会暂停 HTML 文档的解析,并且必须加载、解析和执行 JavaScript 代码。原因是什么?因为 JavaScript 可以使用 document.write()
之类的内容更改整个 DOM 结构之类的内容来更改文档形状。
这里可以延展出defer、async属性、内联和外联的script、JavaScript 中访问了某个元素的样式
首先看内联script,DOM解析器会先执行JavaScript脚本,执行完成之后,再继续往下解析。
遇到外联script,当DOM解析器解析到JavaScript的时候,会先暂停DOM解析,并下载js文件,下载完成之后执行该段JS文件,然后再继续往下解析DOM。
- 如果外联script标签上存在defer属性,defer是js的加载和dom的执行并行,但会等到dom执行完,js代码才会执行。
- 如果外联script标签上存在async属性,async也是js的加载和dom并行,但会在js执行完后阻塞dom的执行,转而执行js代码。
最后当JavaScript中访问了某个元素的样式,那么这时候需要等待这个样式被下载完成才能继续往下执行,所以在这种情况下,CSS也会阻塞DOM的解析。
样式计算
拥有 DOM 并不足以知道页面的显示效果,因为我们可以在 CSS 中设置页面元素的样式。主线程解析 CSS 并确定每个 DOM 节点的计算样式。
1)把 CSS 转换为浏览器能够理解的结构
CSS 样式来源主要有三种:
- 通过 link 引用的外部 CSS 文件
- 标记内的 CSS
- 元素的 style 属性内嵌的 CSS
和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。我们可以在 Chrome 控制台中查看其结构,只需要在控制台中输入 document.styleSheets。
注意style 属性内嵌的 CSS不会被解析到styleSheets中,内联样式是针对单个HTML元素的,具有最高的优先级,仅在定义它们的元素上生效。
2)转换样式表中的属性值,使其标准化
属性值标准化是浏览器解析CSS并渲染网页过程中的一个重要步骤。它确保了CSS属性值以一种统一和可预测的方式被解析和渲染。
- 字体大小标准化
body { font-size: 2em }
2em 依赖于根元素的字体大小,如果根元素(通常是html元素)的字体大小是16px,则 2em 会被标准化为 32px。
body { font-size: 32px }
- 颜色标准化
p { color: blue; }
blue 是一个颜色关键字,它会被转换为一个具体的颜色值,通常是 #0000FF(RGB格式)。
p { color: rgb(0,0,255); }
- 显示属性标准化
span { display: none; }
none 是一个关键字,它指示浏览器不显示该元素,并且在布局中不为其分配空间。
这个值不需要转换,但它会被浏览器解释为一个特定的行为,即不渲染该元素。
- 字体权重标准化
div { font-weight: bold; }
bold 是一个关键字,通常会被标准化为数字 700,在CSS中,400 对应于 “normal”,而 700 对应于 “bold”。
div { font-weight: 700; }
- 层叠和继承的标准化
div p { color: green; }
div { color: red; }
这里涉及到CSS的层叠规则。div p 的选择器比 div 更具体
因此,如果 <p> 元素是 <div> 的直接子元素,它的颜色会被标准化为 green,而不是 red。
如果 <p> 不是直接的子元素,那么它可能会继承 red 这个颜色值。
div p { color: rgb(0,128,0); }
div { color: rgb(255,0,0); }
即使未提供任何 CSS,每个 DOM 节点都有一个计算样式。<h1>
标记的显示区域大于 <h2>
标记,并为每个元素定义了外边距。这是因为浏览器具有默认的样式表。
布局阶段——LayoutTree
现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面。因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
假设您正尝试通过电话向朋友描述一幅画。“有一个大的红色圆圈和一个小的蓝色方块”是不够的信息,不足以让您的朋友知道这幅画究竟是什么样子。
具体过程:
主线程会遍历 DOM 和计算出的样式,并创建布局树,其中包含 x y 坐标和边界框大小等信息。布局树的结构可能与 DOM 树类似,但它仅包含与页面上可见内容相关的信息。如果应用了 display: none
,则该元素不属于布局树(然而,具有 visibility: hidden
的元素在布局树中)。同样,如果应用包含类似 p::before{content:"Hi!"}
的伪元素,它就会包含在布局树中,即使它不在 DOM 中也是如此。
分层——LayerTree
拥有 DOM、样式和布局仍然不足以渲染页面。因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。可以打开 Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况。
- 拥有层叠上下文属性的元素会被提升为单独的一层。
- 需要剪裁(clip)的地方也会被创建为图层。
绘制记录表
拥有 DOM、样式和布局仍然不足以渲染页面。假设您正尝试复制一幅画。您已经知道元素的大小、形状和位置,但仍需判断它们的绘制顺序。
例如,系统可能会为某些元素设置 z-index
,在这种情况下,按 HTML 中编写的元素的顺序进行绘制会导致呈现错误。
渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:
栅格化
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:
如上图所示,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit) 给合成线程。
合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512。然后会将视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
合成和显示
一旦所有图块都被光栅化后,合成器线程会获得这些draw quards图块信息,合成器线程将这些信息生成合成器帧,通过IPC管道传递给浏览器进程,浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
如果发生滚动事件,合成器线程会再创建一个合成器帧以发送到 GPU。
合成的好处是,在不涉及主线程的情况下完成。合成器线程不需要等待样式计算或 JavaScript 执行。因此,仅合成动画被认为是实现流畅性能的最佳选择。如果需要再次计算布局或绘制,则必须涉及主线程。
参考文档: