从输入URL到页面展示的过程?

176 阅读13分钟

缘起

一道无比经典的面试题:从输入URL到页面展示的过程?

个人认为这是前端工程师必知必会的一道题,这道题不仅考察广度又考察深度,能够将前端各种零散的知识点串联起来。

由此,我想借着这道题由浅到深地挖掘每个重要知识点,来逐步搭建并完善自己的前端知识体系架构。

所以这里需要先从广度出发,了解整个过程都发生了什么事,把每个子过程作为前端的一个知识点,这样就能够将子过程串联起来形成我们的前端知识体系架构。再从子过程出发,剖析每个知识点,达成深度。

不过对于计算机方面的知识是无穷无尽的,所以这里的话是针对前端向的重点知识。

骨架

那么我们就从这道题来看看它涉及到了哪些前端知识点,以便搭建我们的前端知识体系架构

  1. 从浏览器接收 url 到开启网络进程准备发送请求(涉及到浏览器机制、进程和线程、强缓存相关)
  2. 与服务器建立连接并由网络进程发出 http 请求(涉及到 DNS、TCP、HTTP、其他计算机网络协议)
  3. 从服务器接收到请求到对请求进行处理(可能涉及到CDN、代理服务器、负载均衡、域名分片等技术)
  4. 从服务器发出响应到浏览器接收响应(涉及到HTTP响应、协商缓存相关、浏览器存储cookie)
  5. 浏览器接收HTTP数据后的解析流程(涉及到解析 html 和 css、html和css相关知识)
  6. 从生成布局树、构建图层树、生成绘制列表、合成到页面展示(涉及到渲染关键路径、浏览器机制)
  7. JS 引擎解析过程(JS 如何执行一段代码、EventLoop)

当然,除了上面列出的几个过程之外,还会有很多其他的过程。比如请求可能会遇到跨域的问题、web安全、每个过程发生的时间点有可能就是一个性能指标和各个过程都能做出相应的性能优化等等,只要你想深挖,真的是无穷无尽。

本篇的话只是梳理整个流程,其中涉及到的其他知识点有时间再补充。

image-20210224225020159.png

网络请求篇

1. 构建 URL

当用户在地址栏输入一个查询关键字时,地址栏会判断输入的关键字是搜索内容还是请求URL。

  • 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎去合成URL
  • 如果是请求URL,例如 www.baidu.com,那么地址栏会给这段 URL 加上协议,合成为完整的URL https://www.baidu.com

完整的 URL 主要由:协议、主机、端口、路径、查询参数构成

2. 查找强缓存

浏览器进程会通过进程间通信(IPC)把 URL 发送给网络进程,网络进程接收到 URL 并不会马上发送请求,它会先尝试查找本地缓存

  • 如果有缓存资源,那么直接将缓存资源返回给浏览器进程。
  • 如果没有缓存,那么进入网络请求流程。

3. DNS 解析

由于我们输入的一般是域名,而数据包是通过 IP 地址传给对方的。因此我们需要得到域名对应的 IP 地址。这个过程就需要用到 DNS 域名解析系统

另外,浏览器提供了DNS缓存功能,如果一个域名已经解析过了,那么浏览器会将解析结果缓存起来,下次处理时直接从缓存中拿取数据,不需要经过 DNS 解析。

整个 DNS 查询过程如下:

  1. 首先查找浏览器 DNS 缓存,如果有,则域名解析流程结束;否则进入下一步
  2. 读取操作系统的 hosts 文件查找是否有对应的映射关系,有的话流程结束;否则进入下一步
  3. 查找本地域名服务器,有的话流程结束;否则进入下一步
  4. 根域名服务器查询顶级域(例如 .com)的 IP 地址,然后在向顶级域名服务器查询二级域名(例如 baidu.com)的 IP 地址,最后向权威域名服务器查询完整域名的 IP 地址。

4. 建立 TCP 连接

由于 HTTP 请求是建立在 TCP/IP 之上的,所以需要先建立 TCP 连接。

对于 TCP 建立连接的过程,是通过三次握手进行协商建立的,具体如下。

image-20210214003229256.png

5. 建立 TLS 连接

如果此时发现请求协议是 HTTPS,那么还需要建立 TLS 连接。

TLS 握手是为了证明服务器的真实性以及协商出会话密钥

image-20210226143555245.png

6. 发送 HTTP 请求

完成 TCP 连接之后,浏览器可以与服务器开始通信了,也就可以发送 HTTP 请求。浏览器会构建请求报文,而请求报文一般由请求行请求头请求体等信息组成。

请求行的格式如下,其中 GET 为请求方法,路径为根路径,HTTP 协议版本为 1.1

GET / HTTP/1.1

请求头的格式如下,另外对于 Cache-ControlIf-Modified-SinceIf-None-Match 都有可能会放入请求头信息中去向服务器询问缓存的情况。当然也有其他请求头,例如紧跟域名的 Cookie

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cache-Control: max-age=0
Connection: keep-alive
Cookie: cookie
Host: www.baidu.com
sec-ch-ua: "Google Chrome";v="87", " Not;A Brand";v="99", "Chromium";v="87"
sec-ch-ua-mobile: ?0
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36

对于请求体的话,请求体存在于 POST 方法中。

7. 响应 HTTP 请求

对于 HTTP 响应报文,它同样由响应行响应头响应体构成。

响应行格式:由 HTTP 协议版本、状态码和状态描述组成

HTTP/1.1 200 OK

响应头类似下面这样,对于 Cache-ControlExpiresLast-ModifiedETag 都有可能会放入响应头信息中通知浏览器如何缓存这些资源。当然还有很多其他头信息,例如 Set-Cookie 来设置 Cookie 信息。

Bdpagetype: 2
Bdqid: 0xed681f84001f87c9
Cache-Control: private
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html;charset=utf-8
Date: Fri, 12 Feb 2021 17:16:00 GMT
Expires: Fri, 12 Feb 2021 17:16:00 GMT
Server: BWS/1.1
Set-Cookie: H_PS_PSSID=33423_33429_33272_31660; path=/; domain=.baidu.com
Strict-Transport-Security: max-age=172800
Traceid: 1613150160034475034617106957836365039561
Transfer-Encoding: chunked
X-Ua-Compatible: IE=Edge,chrome=1

响应头信息中还有一个重要的字段 Content-Type,它告诉浏览器服务器返回的响应体数据是什么类型。如果 Content-Type 的值为 text-html,那么会通知浏览器进程准备渲染进程准备渲染。

另外,如果状态码是301或302,那么会进行重定向,从 Location 字段读取新的URL,然后重新发起请求。

浏览器解析篇


当接收到的 Content-Type 为 html 类型时,接下来就是由渲染进程来负责解析渲染工作了。

对于解析的流程,主要分为以下几个步骤:

  • 解析 HTML 构建 DOM 树
  • 解析 CSS 进行样式计算

1. 解析 HTML

渲染引擎内部有一个 HTML 解析器(HTMLParser) 的模块,它负责 HTML 的解析。

对于 HTML 解析算法,具体分为两个过程:

  1. Token 化(词法分析)
  2. 构建 DOM 树(语法分析)

Token 化

简单来说,Token 化的过程就是将 HTML 文本分解为一个个 Token,Token 即 HTML 标记。其过程使用了一种类似状态机的算法

<html>
  <body>
    Hello World
  </body>
</html>

例如上面的例子,整个标记的过程:

  1. 匹配到 <,状态切换为标签打开状态
  2. 匹配 [a-z] 的字符,状态切换为标签名称状态。这个状态会一直保持,直到匹配到 >
  3. 匹配到 >,表示标签名称记录完成,状态切换为数据状态
  4. 接下来遇到 body 标签也是同样的处理,最后来到 >,状态为数据状态
  5. 保持数据状态接收字符串 Hello World
  6. 匹配到 <,状态切换为标签打开状态,匹配到 /,状态切换为结束标签打开状态
  7. 后续过程与上面一样,直至完成

最终生成的 Token 如下

StartTag html
    StartTag body
        Hello World
    EndTag body
EndTag html

构建 DOM 树

在上述的 Token 化过程中产生的每个 Token 都会被传输到树构建器中进行处理,树构建器会利用一个开放元素栈,进而为 Token 生成一个 DOM 节点,以便有序地插入到 DOM 树中。

StartTag html
    StartTag body
        Hello World
    EndTag body
EndTag html
  1. 在开始工作之前,会默认创建一个以 document 为根的空 DOM 结构,同时会维护一个 Token 栈
  2. 当树构建器接收到 StartTag html 时,会为该 Token 生成一个 html 节点并插入到 document 节点上,同时将该 Token 压入栈
  3. 接着收到 StartTag body,由于没有接收到 head 的 Token,所以会自动创建一个 head 节点。
  4. 收到 StartTag body 后,生成 body 节点插入到父节点(父节点就是栈顶节点)同时压入栈中
  5. 收到文本 Token Hello World,将文本节点插入父节点中,但是不用压栈
  6. 收到 EndTag body,将栈顶弹出,表示 body 元素解析完成
  7. 收到 EndTag html,将栈顶弹出,此时解析过程结束

DOM 树是一颗以 document 为根节点的多叉树。在 JS 中可使用 document 查看

2. 解析 CSS 并计算样式

关于 CSS 的来源,主要来自以下三种:

  1. 通过 link 引用的 css 文件
  2. style 标签内的 css
  3. 元素的内嵌 style 属性

格式化样式表

与 HTML 一样,浏览器也是无法理解 CSS 文本内容的,所以当解析 HTML 的过程中遇到 CSS 时,会去解析 CSS 转换为浏览器能够理解的结构,即 styleSheets。

我们可以通过在控制台输入 document.styleSheets 进行查看,这个结构就包含了上面三种 CSS

标准化样式属性

有一些 CSS 属性,如 1em、blue、bold,这些数值不被理解,所以需要将这些值转为渲染引擎能够理解的,标准化的值,如 16pxrgb(0, 0, 255)700

计算样式

接下来计算每个 DOM 节点的样式属性,其中涉及到 CSS 的继承规则和层叠规则

CSS 具有继承特性,也就是会继承父节点样式作为当前样式,然后再进行覆盖。每个节点的样式中还包含有浏览器提供的默认样式。

关于层叠特性,它是一个定义了如何合并来自多个源的属性值,即最终的样式取决于各个属性共同作用的效果。

最终会计算出每个 DOM 节点的样式属性,这个我们可以打开开发者工具的 element -> Computed 面板进行查看

题外话:阻塞

从上面的解析过程中我们知道解析html构建DOM树,在解析的过程中难免会遇到 css 和 js,那么我们思考这个问题

css、js 会阻塞 html 的解析,也就是 DOM 的生成吗?会阻塞页面的渲染吗?

  • css 在部分情况下也会阻塞DOM的生成
  • js会阻塞DOM生成
  • css、js 都会阻塞页面渲染

由于 js 内部经常会获取 DOM 节点和样式,所以在解析 html 的过程中如果遇到 js 文件会暂停解析,将控制权交由 js 引擎线程,并等待其下载和执行完成后再继续解析。如果有引用css文件,js文件不会立即执行,而是等css文件解析完构建CSSOM之后才执行。

所以css在部分情况下也会阻塞DOM的生成,js会阻塞DOM生成,两者都会阻塞页面渲染

优化

浏览器在解析 html 构建 DOM 树时,这个过程占用了主线程。

浏览器针对下载阶段进行优化,渲染引擎中有预加载扫描器,当接收到 html 文件时,扫描器会对文件进行扫描,并找到关键资源(如 css, js)去下载,下载过程中不会占用主线程,减少阻塞时间。

另外,我们经常把css文件放在head,而js文件放在body底部,避免 js 执行时 css 还未解析完成。

或者通过 async 和 defer 来标记 js 文件,从而避免下载过程阻塞DOM生成

  • async:js文件异步下载,下载完成立即执行。
  • defer:推迟下载,等到 DOMContentLoaded 事件之后执行。

关于 DOMContentLoaded 和 load

  • DOMContentLoaded 事件:当纯HTML被完全加载以及解析时,DOMContentLoaded 事件会被触发,而不必等待样式表,图片或者子框架完成加载。也就是 jQuery 中的 $(document).ready(function() { });
  • load 事件:等所有资源加载完成之后(需要等待样式表,图片或者子框架等资源完成加载),load 事件才会被触发。

关于上面这两者的区别,我们可以点击进入这个站点来观察一下

另外,我们可以打开 NetWork 面板来查看这两者执行的具体时刻,如下图,蓝色的线是 DOMContentLoaded 执行时刻,红色的线是 load 执行时刻。

image-20210802005909948.png

浏览器渲染篇

1. 构建布局树

现在,我们已经有了 DOM 树和 DOM 树中的样式。接下来就是确定 DOM 树中可见元素的几何位置,这个过程就是构建布局树。

构建布局树具体有两个步骤:

  1. 遍历 DOM 树中的所有可见节点,并添加到布局树中
  2. 计算布局树中每个节点的位置信息

这样我们就有了一颗完整的布局树,这棵布局树只有可见节点,例如 headdisplay: none 的节点都没有在布局树上

2. 构建图层树

接下来,渲染引擎为了应对复杂的场景,比如3D变换、页面滚动或者使用 z-index 做为 z 轴对页面排序等。渲染引擎为特定的节点生成专用的图层,并生成一颗图层树。这些图层经过叠加后最终合成为完整的页面。(这里的图层指的是合成层

合成层

在了解合成层前需要先了解下渲染层,由下图可以得知,一个 DOM 节点对应一个渲染对象,由渲染对象根据层叠上下文的划分,处于相同坐标空间(z 轴空间)的渲染对象,都将归于同一个渲染层中,而不同坐标空间的渲染对象将形成多个渲染层。

16daf0c0a72be715.jpg

当满足某些条件的渲染层将被提升为合成层,合成层能给我们带来不少好处:

  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他层
  • 合成层的位图能够交由 GPU 合成

那么如何将渲染层提升为合成层呢?有两种情况,一种是显式合成,一种是隐式合成

显式合成

我们可以通过设置某些 CSS 属性来提升为合成层

  • 通过 3D transforms:translate3d、translateZ
  • video、canvas、iframe 等元素
  • 通过 CSS 动画实现 opacity 或 transform 动画的转换
  • position: fixed 或 sticky
  • 具有 will-change 属性,且值为 opacity、transform、top、left、bottom、right
  • 需要进行裁剪的地方

隐式合成

层叠等级低的渲染层被提升为单独的合成层后,那么层叠等级高的渲染层都将提升为合成层。

隐式合成如果处理不当会有层爆炸的风险,若一个 z-index 很低的渲染层被提升为合成层后,层叠在它上面的渲染层都将提升为合成层,那么可能会产生几千个合成层。

3. 生成绘制列表

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

我们可以通过打开开发者工具的 Layers 面板中的 Profiler 进行查看,下面以谷歌首页为例

image-20210223004030284.png

可以看到,谷歌首页被分为了两个层(document 和 滚动条),其中的 Profiler 就是一条条绘制指令

4. 生成图块、位图以及合成

一旦有了图层树以及对应的绘制列表之后就能进行绘制操作了,而绘制操作是交由渲染进程中的合成线程来执行的。

合成线程将图层分成一个个小块,并将这些图块交给栅格线程来生成位图,由图块生成位图就是栅格化。渲染进程维护了一个栅格化线程池,所有图块的栅格化都在线程池中进行。

另外,合成线程能够对栅格化线程池进行优先排序,以便利用视口附近的图块来优先生成位图。

当生成合成层的位图之后,会交由 GPU 将多个位图进行合成,这也就是常说的 GPU 硬件加速

5. 显示页面

栅格化后合成线程发送命令 "draw quad" 给浏览器进程后,浏览器进程将合成帧发送给显卡。

显卡的刷新频率一般是 60Hz,即一秒更新60帧图像,而显卡分为前缓冲区后缓冲区,每次更新帧的时候是将帧发送给显卡的后缓冲区,显示的区是前缓冲区,当显卡将图像保存在后缓冲区,会自动将前缓冲区和后缓冲区对换位置,以达到页面更新。

这也就是为什么在执行一次动画过程中,当渲染引擎生成某些帧的时间过久,帧传给显卡不及时,而显卡的刷新频率不变,从而产生页面卡顿的现象。