跳到主要内容

认识

2024年03月21日
柏拉文
越努力,越幸运

一、认识


一、URL 解析, 浏览器首先会将输入的 URL 拆解成协议(如 HTTP/HTTPS)、域名、端口号(若指定)、路径和查询参数等部分。

二、检查 HTTP 缓存, 在发起请求之前,浏览器首先检测请求资源是否在缓存中并且新鲜。如果资源未缓存, 发起新请求; 如果已缓存, 检验是否足够新鲜, 足够新鲜直接提供给客户端, 否则与服务器进行验证。HTTP 检测强缓存、协商缓存工作流

三、检查 DNS 缓存、DNS 解析, HTTP 发起请求之前, 浏览器需要通过 TCP 与服务器建立连接, 而 TCP连接 需要用到 IP端口号, 所以浏览器会请求 DNS 解析域名对应的 IPDNS 采用了分布式、分层的架构,通过多个服务器协同工作来完成解析。每个域名记录都有一个 TTLTime To Live)值,表示该记录在缓存中可保存的时间,以减少重复查询的负担。首先检查浏览器缓存中是否有该域名对应的 IP 缓存记录。如果浏览器没有命中,会查询操作系统的 DNS 缓存。在某些场景下, 操作系统还会首先查找本地的 hosts 文件,该文件可以将特定域名映射到预定义 IP。如果缓存中存在且未过期,则直接返回结果,无需向外部 DNS 服务器发起查询。如果本地缓存没有命中, DNS 会通过递归查询或者迭代查询来获取最终的 IP 地址或错误信息。DNS 解析过程

四、等待 TCP 队列, 通过 DNS解析 获取到 IP 后, 接下来就需要获取端口号了。通常情况下, 如果 URL 没有特别指明端口号, 那么 **HTTP**协议默认是 80 端口。 HTTPS 协议默认是 443 端口。 IP端口号 都准备好后, 进入TCP队列

  • 如果使用 http/1.1, 一个 TCP 同时只能处理一个请求,浏览器会为每个域名维护 6TCP 连接。 但是每个 TCP 连接是可以复用的, 也就是处理完一个请求之后, 不断开这个 TCP 连接, 可以用来处理下个 HTTP 请求。如果在同一个域名下同时有 10 个请求发生, 那么其中 4 个请求会进入排队等待状态, 直至进行中的请求完成。如果当前请求数量少于 6, 会直接进入下一步,建立 TCP 连接。

  • 如果使用 http/2.0: 采取多路复用,在一个 TCP 连接,客户端和浏览器都可以同时发送多个请求或回应。所以如果使用 HTTP2,浏览器只会为每个域名维护一个 TCP 连接。

五、三次握手, 建立 TCP 连接, 三次握手(Three-way Handshake 其实就是指建立一个 TCP 连接时, 需要客户端和服务器总共发送 3 个包。进行三次握手的主要作用就是 为了确认双方的接收能力和发送能力是否正常指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是 连接服务器指定端口建立TCP连接并同步连接双方的序列号和确认号交换TCP窗口大小信息。最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。然后服务端开始监听某个端口,进入 LISTEN 状态。 TCP 三次握手建立连接

  • 第一次握手: 客户端发送一个 TCPSYN=1Seq=X 的包到服务器端口 。客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SEND 状态。首部的同步位SYN=1,初始序号seq=xSYN=1 的报文段不能携带数据,但要消耗掉一个序号。==> (SYN=1, seq=x)

  • 第二次握手: 服务器发回 SYN=1ACK=X+1Seq=Y 的响应包。 服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。在确认报文段中SYN=1ACK=1,确认号ack=x+1,初始序号seq=y。 ==> (SYN=1, ACK=1, seq=y, ACKnum=x+1)

  • 第三次握手: 客户端发送 ACK=Y+1Seq=Z。客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。==>(ACK=1,ACKnum=y+1)

六、HTTPS 连接: 在 TCP 建立连接之后, HTTPS 连接主要通过 TLS(或 SSL)握手来建立一个加密信道。HTTPS 连接过程

七、发送 HTTP 请求

八、Node BFF 层处理请求: Node BFF 可能需要进行鉴权、代理请求、转发请求操作。

九、服务端响应请求: 服务器接受请求并解析, 将请求转发到服务程序, 如虚拟主机使用 HTTP Host 头部判断请求的服务程序。服务器检查 HTTP 请求头是否包含缓存验证信息如果验证缓存新鲜,返回304等对应状态码。处理程序读取完整请求并准备HTTP响应,可能需要查询数据库等操作。服务器将响应报文通过 TCP 连接发送回浏览器

十、客户端接收 HTTP 响应, 然后根据情况选择关闭 TCP 连接或者保留重用

十一、四次挥手, 断开 TCP 连接: 建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由 TCP半关闭half-close)造成的。所谓的半关闭,其实就是 TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。通常情况下, 一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了 Connection:Keep-Alive, 那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。TCP 四次挥手断开连接

十二、客户端处理响应: 1. 在接收到服务器返回的响应行后,网络进程开始解析响应行; 2. 解析完响应行之后,浏览器通过 Content-Type 来决定如何显示响应体的内容。

  1. 在接收到服务器返回的响应行后,网络进程开始解析响应行。浏览器检查响应状态码: 是否为 1XX3XX4XX5XX,这些情况处理与 2XX 不同。如果资源可缓存, 进行缓存。对响应进行解码(例如gzip压缩)。根据资源类型决定如何处理(假设资源为HTML文档)

    • 服务器返回的响应行的状态码是 301 或者 302, 那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。在导航过程中,如果服务器响应行的状态码包含了 301302 一类的跳转信息,浏览器会跳转到新的地址继续导航

    • 服务器返回的响应行的状态码是 200,这是告诉浏览器一切正常,可以继续往下处理该请求了。

  2. 解析完响应行之后,浏览器通过 Content-Type 来决定如何显示响应体的内容。

    • Content-type 字段的值是text/html: 告诉浏览器,服务器返回的数据是HTML格式,浏览器开始渲染

    • Content-Type 的值是application/octet-stream: 显示数据是字节流类型的,通常情况下,浏览器会按照下载类型来处理该请求。

十三、如果资源类型为 HTML, 解析 HTML 文档, 构建 DOM: 为该请求选择或者创建一个渲染进程, 渲染进程准备好之后, 网络进程和渲染进程之间会建立一个共享数据的管道, 网络进程接收到数据后就往这个管道里面放, 而渲染进程则从管道的另外一端不断地读取数据, 在渲染引擎内部, 有一个叫 HTML 解析器HTMLParser)的模块, 将读取的数据从 字节流 转换为 DOM。解析的过程中, 解析数据与网络进程加载数据同时进行,并不是等着整个 HTML 文档加载完成后在开始解析。

  1. HTML 解析器 通过分词器将字节流转换为一个个Token。解析后的Token分为Tag Token文本 Token,Tag Token又分为StartTagEndTagToKen词语生成之后,随之被XSSAuditor过滤。它会检测这些模块是否安全,比如是否引用了外部脚本, 是否符合 CSP 规范,是否存在跨站点请求等。然后将 Token 解析为 DOM 节点, 并将 DOM 节点添加到 DOM 树中。HTML解析器 维护了一个Token栈结构,该Token栈主要用来计算节点之间的父子关系,在第一个阶段中生成的 Token 会被按照顺序压到这个栈中。通过 分词器 产生的新Token就这样不停地压栈和出栈, 整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。

  2. DOM 构建完成之后,浏览器触发 DOMContentLoaded(onready) 事件,注册再该事件上的JavaScript函数会被调用。当所有资源加载完之后,浏览器会触发 onload 事件。

十四、HTML 解析器解析到 script

  1. HTML 解析器解析到没有 asyncdeferscript: 渲染引擎判断这是一段脚本。因为接下来的 JavaScript 脚本可能要修改当前已经生成的 DOM 结构HTML解析器暂停解析。所以 JavaScript 代码的执行会阻碍后面 DOM节点 的创建同时也会阻碍后面的资源下载。如果遇到 外联脚本JavaScript执行 之前还需等待下载完成。

    • 在执行 JavaScript 之前,需要先解析 JavaScript 语句之上所有的 CSS 样式。所以如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行**JavaScript** 脚本。而 JavaScript引擎 在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。

    • 同样,在执行 JavaScript 之前,浏览器通过 预先扫描器 来扫描 script 之后的词语,如果发现有其他资源,那么浏览器会通过 预资源加载器来发送请求,来提前下载资源。

  2. HTML 解析器解析到有 async 属性的 script: 开始下载脚本并继续解析 HTML。脚本会在它下载完成后尽快执行, 但是解析器不会停下来等它下载。

  3. HTML 解析器解析到有 defer 属性的 script: 开始下载脚本并继续解析 HTML。文档完全解析完成, 所有 defer 脚本会按照在文档出现的顺序执行

十五、HTML 解析过程中的预解析: JavaScript 文件的 下载过程执行过程 会阻塞 DOM 解析。而执行JavaScript 文件之前都会下载 Css 文件。而通常下载又是非常耗时的,会受到网络环境、JavaScriptCss 文件大小等因素的影响。Chrome 浏览器做了很多优化,其中一个主要的优化是 预解析操作。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScriptCSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。

十六、构建 CSSOM: 和 HTML 一样,渲染引擎也是无法直接理解 CSS 文件内容的,所以需要将其解析成渲染引擎能够理解的结构,这个结构就是 CSSOM。和 DOM 一样, CSSOM 也具有两个作用: 1. 提供给 JavaScript 操作样式表的能力; 2. 为布局树的合成提供基础的样式信息。**CSSOM**体现在 DOM 中就是 document.styleSheets 。可以在 Chrome 控制台中查看其结构,只需要在控制台中输入 document.styleSheets

十七、根据 DOM 树和 CSSOM 树构建渲染树: 1. 从 DOM 树的根节点遍历所有可见节点, 不可见节点包括: script, meta 这样本身不可见的标签、被 css 隐藏的节点, 如 display: none; 2. 对每一个可见节点,找到恰当的 CSSOM 规则并应用; 3. 发布可视节点的内容和计算样式

十八、布局计算: 构建完渲染树之后, 计算渲染树中节点的坐标位置。在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。

十九、分层: 现在有了渲染树, 而且每个元素的具体位置信息都计算出来了,此时还是不可以绘制页面了。因为页面中有很多复杂的效果,如一些复杂的 3D 变换页面滚动,或者使用 z-indexz 轴排序等,为了更加方便地实现这些效果, 渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的 图层树(LayerTree

二十、绘制: 在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制

二十一、分块: 绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。当图层绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程.在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512。

二十二、光栅化: 合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。通常,栅格化过程都会使用GPU来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。

二十三、合成: 一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。

二、问题


2.1 浏览器渲染流程?

2.2 输入 URL 之后的过程?

2.3 DOM 树和渲染树的区别?

**DOM 树(文档对象模型树)**是由 HTML 文档解析而来,它反映了文档的结构和内容,包括 HTML 标签、文本节点和注释等。DOM 树中的每个节点都有其对应的 CSS 样式规则。

渲染树(Render Tree 是由 DOM 树和 CSSOM 树合并而成,它是用于显示在浏览器中的树状结构。渲染树只包含需要显示在页面上的节点,不包含隐藏的节点和不可见的节点。渲染树中的每个节点都有其对应的布局信息和样式信息,用于计算节点在屏幕中的位置和大小,并将节点绘制到屏幕上。

总结: DOM 树表示了 HTML 文档的结构和内容,而渲染树是为了将文档在浏览器中显示而构建的树结构。渲染树 只包含需要显示的节点,并且每个节点都有对应的布局和样式信息,用于计算和绘制节点在屏幕上的位置和外观。

2.4 script 标签位置是否会影响首屏显示?

默认情况下,浏览器在遇到 <script> 标签时,会暂停 HTML 的解析, 虽然浏览器会有预解析, 会提前加载 script, 但是也需要等到 script 脚本执行完成后, 才继续 HTML 解析。从而阻塞页面的解析和渲染。

如果没有 deferasync, 将 <script> 放在 <head> 中, 浏览器会在渲染页面前加载并执行这些脚本,从而延迟首屏显示。如果将 <script> 放在页面底部(比如紧靠 </body> 标签之前),则页面的主要内容可以先加载并呈现出来,脚本在后续加载执行,不会阻塞首屏渲染。

如果有 async 属性时, 允许脚本异步加载, 但一旦加载完成会立即执行,仍可能中断渲染。

如果有 defer 属性时, 允许脚本异步加载, 且在 HTML 全部解析完毕后才执行,从而不阻塞页面渲染。

2.5 script 的加载会阻塞浏览器渲染吗?

默认情况下,浏览器在遇到 <script> 标签时,会暂停 HTML 的解析, 虽然浏览器会有预解析, 会提前加载 script, 但是也需要等到 script 脚本执行完成后, 才继续 HTML 解析。从而阻塞页面的解析和渲染。

如果没有 deferasync, 将 <script> 放在 <head> 中, 浏览器会在渲染页面前加载并执行这些脚本,从而延迟首屏显示。如果将 <script> 放在页面底部(比如紧靠 </body> 标签之前),则页面的主要内容可以先加载并呈现出来,脚本在后续加载执行,不会阻塞首屏渲染。

如果有 async 属性时, 允许脚本异步加载, 但一旦加载完成会立即执行,仍可能中断渲染。

如果有 defer 属性时, 允许脚本异步加载, 且在 HTML 全部解析完毕后才执行,从而不阻塞页面渲染。

2.6 script 放在 head 里和放在 body 里有什么区别?

JavaScript 代码放在 <head> 标签内部和放在 <body> 标签内部有一些区别:

  1. 加载顺序:放在 <head> 里会在页面加载之前执行 JavaScript 代码, 而放在<body> 里会在页面加载后执行。

  2. 页面渲染: 如果 JavaScript 代码影响了页面的布局或样式, 放在 <head> 里可能会导致页面渲染延迟,而放在 <body> 里可以减少这种影响。

  3. 代码依赖: 如果 JavaScript 代码依赖其他元素,放在 <body> 里可以确保这些元素已经加载。

  4. 全局变量和函数: 放在 <head> 里的 JavaScript 代码中的全局变量和函数在整个 页面生命周期内都可用。

如果没有 deferasync, 将 <script> 放在 <head> 中, 浏览器会在渲染页面前加载并执行这些脚本,从而延迟首屏显示。如果将 <script> 放在页面底部(比如紧靠 </body> 标签之前),则页面的主要内容可以先加载并呈现出来,脚本在后续加载执行,不会阻塞首屏渲染。

2.7 在浏览器里,从输入 URL 到页面展示,这中间发生了什么?

2.8 DOM 中 onload 与 DOMContentLoaded(onready) 有什么区别?

onload, 会在整个页面的所有资源(包括 DOM、样式表、图片、脚本等)都加载完成之后触发。由于等待的资源更多,触发时机通常比 DOMContentLoaded 晚。

DOMContentLoaded, 当 HTML 文档被完全加载和解析完成后,即构建了完整的 DOM 树时,就会触发该事件。此时并不要求样式表、图片或其他外部资源加载完毕,因此触发较早。

2.9 如果下载 CSS 文件阻塞了, 会阻塞 DOM 树的合成吗?会阻塞页面的显示吗?

如果下载 CSS 文件阻塞了, HTML 解析器会继续解析 HTML 并构建 DOM 树, 即使 CSS 文件还在下载中。也就是说, CSS 文件不会直接阻塞 DOM 树的构建过程。但是, 浏览器需要将 DOM 树和 CSSOM 结合生成渲染树, 才能进行布局和绘制。如果 CSS 文件下载阻塞, CSSOM 的生成就会延迟,进而导致渲染树生成推迟, 最终阻塞页面的显示。因此,外部 CSS 文件被阻塞会延迟页面的首次渲染(即所谓的渲染阻塞)。

2.10 把 <script> 放在 </body> 之前和之后有什么区别?浏览器会如何解析它们?

<script>放在</body> 之前和之后的区别主要是在符合 HTML 标准的语法规则和浏览器的容错机制上, 具体如下:

按照HTML标准规定, <script>标签应该放在<body>标签内,通常是放在</body>之前。将<script>放在</body>之后是不符合HTML标准的,属于语法错误。但是,现代浏览器通常会自动容错并解析这样的语法,不会出现明显的错误。

浏览器解析, 浏览器会解析并执行<script>标签中的 JavaScript 代码。无论<script>放在</body>之前还是之后,浏览器都会执行其中的代码。浏览器的容错机制会忽略<script>之前的</body>, 视作<script>仍然在<body>内部。因此,从功能和效果上来说,两者没有区别。

服务器输出优化, 在一些情况下,省略</body></html>闭合标签可以减少服务器输出的内容,因为浏览器会自动补全这些标签。对于大型网站或需要优化响应速度的场景,这种优化可以略微减少传输的字节数。

需要注意的是,虽然现代浏览器对放置<script>标签的位置比较宽容,但为了遵循 HTML 标准和保持代码的可读性和可维护性,推荐将<script>标签放在</body>之前,符合语义和结构的要求。

2.11 background-image 是怎么加载图片的? 与 img 标签相比, 哪种方式先加载? 哪种方式性能更好?

<img>HTML 标签, 当浏览器解析 HTML 时遇到 <img> 标签,就会立即开始请求对应的图片资源。图片请求是并行启动的, 不会阻塞 HTML 的解析, 浏览器会在 <img> 标签所在的位置分配一个区域,直到图片加载完成后才显示具体内容,但整个页面的其他部分依然可以正常渲染。

background-imageCSS 属性,依赖于 CSS 文件的加载与解析。当浏览器解析 CSS 文件, 并遇到 background-image 规则时,就会触发对图片的请求。一般情况下,这个请求不会立即发生在 HTML 解析过程中,而是在 CSS 规则应用到相应元素上时启动。

2.12 Dom onready 与 onload 有什么区别?Image onrady 与 onload 与 DOM onready 与 onload 的先后顺序, 并简述原因

DOM onload, 会在整个页面的所有资源(包括 DOM、样式表、图片、脚本等)都加载完成之后触发。由于等待的资源更多,触发时机通常比 DOMContentLoaded 晚。

DOM DOMContentLoaded(onready), 当 HTML 文档被完全加载和解析完成后,即构建了完整的 DOM 树时,就会触发该事件。此时并不要求样式表、图片或其他外部资源加载完毕,因此触发较早。

Image onload: 图片的 onload 事件是在图片文件完全加载后触发的,此时图片所有数据已经传输完毕,可以完整显示。 onload 确保图片所有资源数据都已经接收,这对于需要保证图片完整显示或进行后续处理(如图片特效)的场景很重要。

Image onready: 用来检测图片的尺寸信息(即图片的元数据)是否已就绪。这一时机通常早于图片完全加载完成的 onload 事件,特别适合于需要预先获取图片宽高以便布局的场景。在图片的加载过程中, 当浏览器解析到足够的信息获取图片的实际尺寸时,就可以认为图片 就绪 了,这比等待整个图片文件传输完成要早。

x事件触发的先后顺序: 在一个典型的页面加载过程中,各事件的触发顺序大致如下:

  1. DOM onready(DOMContentLoaded): 当 HTML 解析完成、DOM 构建完毕时触发,此时不必等待图片、CSS 等加载完成。

  2. 图片 onready(如果使用了类似检测图片尺寸的机制): 对于每个图片,当其元数据或尺寸信息就绪时(通常比文件完全加载早),会触发相应的回调。注意:这不是浏览器的标准事件,而是很多第三方库提供的功能。

  3. 图片 onload: 当单个图片文件全部加载完成后触发。如果图片来自缓存,onload 可能会非常快触发,但原则上依然晚于 就绪 时机。

  4. window onload: 当页面中所有资源(包括所有图片、样式表、脚本等)都加载完成后,才会触发 window onload 事件。

参考资料


阿里面试官的”说一下从url输入到返回请求的过程“问的难度就是不一样!