认识
一、认识
一、URL
解析, 浏览器首先会将输入的 URL
拆解成协议(如 HTTP/HTTPS
)、域名、端口号(若指定)、路径和查询参数等部分。
二、检查 HTTP
缓存, 在发起请求之前,浏览器首先检测请求资源是否在缓存中并且新鲜。如果资源未缓存, 发起新请求; 如果已缓存, 检验是否足够新鲜, 足够新鲜直接提供给客户端, 否则与服务器进行验证。HTTP 检测强缓存、协商缓存工作流
三、检查 DNS
缓存、DNS
解析, HTTP
发起请求之前, 浏览器需要通过 TCP
与服务器建立连接, 而 TCP
连接 需要用到 IP
和 端口号, 所以浏览器会请求 DNS
解析域名对应的 IP
。DNS
采用了分布式、分层的架构,通过多个服务器协同工作来完成解析。每个域名记录都有一个 TTL
(Time To Live
)值,表示该记录在缓存中可保存的时间,以减少重复查询的负担。首先检查浏览器缓存中是否有该域名对应的 IP
缓存记录。如果浏览器没有命中,会查询操作系统的 DNS
缓存。在某些场景下, 操作系统还会首先查找本地的 hosts
文件,该文件可以将特定域名映射到预定义 IP
。如果缓存中存在且未过期,则直接返回结果,无需向外部 DNS
服务器发起查询。如果本地缓存没有命中, DNS
会通过递归查询或者迭代查询来获取最终的 IP
地址或错误信息。DNS 解析过程
四、等待 TCP
队列, 通过 DNS
解析 获取到 IP
后, 接下来就需要获取端口号了。通常情况下, 如果 URL
没有特别指明端口号, 那么 **HTTP
**协议默认是 80
端口。 HTTPS
协议默认是 443
端口。 IP
和 端口号 都准备好后, 进入TCP队列。
-
如果使用
http/1.1
, 一个TCP
同时只能处理一个请求,浏览器会为每个域名维护6
个TCP
连接。 但是每个TCP
连接是可以复用的, 也就是处理完一个请求之后, 不断开这个TCP
连接, 可以用来处理下个HTTP
请求。如果在同一个域名下同时有10
个请求发生, 那么其中4
个请求会进入排队等待状态, 直至进行中的请求完成。如果当前请求数量少于6
, 会直接进入下一步,建立TCP
连接。 -
如果使用
http/2.0
: 采取多路复用,在一个TCP
连接,客户端和浏览器都可以同时发送多个请求或回应。所以如果使用HTTP2
,浏览器只会为每个域名维护一个TCP
连接。
五、三次握手, 建立 TCP
连接, 三次握手(Three-way Handshake
) 其实就是指建立一个 TCP
连接时, 需要客户端和服务器总共发送 3
个包。进行三次握手的主要作用就是 为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是 连接服务器指定端口,建立TCP
连接,并同步连接双方的序列号和确认号,交换TCP
窗口大小信息。最开始的时候客户端和服务器都是处于CLOSED
状态。主动打开连接的为客户端,被动打开连接的是服务器。然后服务端开始监听某个端口,进入 LISTEN
状态。 TCP 三次握手建立连接
-
第一次握手: 客户端发送一个
TCP
的SYN=1
,Seq=X
的包到服务器端口 。客户端给服务端发一个SYN
报文,并指明客户端的初始化序列号ISN
。此时客户端处于SYN_SEND
状态。首部的同步位SYN=1
,初始序号seq=x
。SYN=1
的报文段不能携带数据,但要消耗掉一个序号。==>(SYN=1, seq=x)
-
第二次握手: 服务器发回
SYN=1
,ACK=X+1
,Seq=Y
的响应包。 服务器收到客户端的SYN
报文之后,会以自己的SYN
报文作为应答,并且也是指定了自己的初始化序列号ISN
。同时会把客户端的ISN + 1
作为ACK
的值,表示自己已经收到了客户端的SYN
,此时服务器处于SYN_REVD
的状态。在确认报文段中SYN=1
,ACK=1
,确认号ack=x+1
,初始序号seq=y
。 ==>(SYN=1, ACK=1, seq=y, ACKnum=x+1)
-
第三次握手: 客户端发送
ACK=Y+1
,Seq=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
来决定如何显示响应体的内容。
-
在接收到服务器返回的响应行后,网络进程开始解析响应行。浏览器检查响应状态码: 是否为
1XX
,3XX
,4XX
,5XX
,这些情况处理与2XX
不同。如果资源可缓存, 进行缓存。对响应进行解码(例如gzip
压缩)。根据资源类型决定如何处理(假设资源为HTML
文档)-
服务器返回的响应行的状态码是
301
或者302
, 那么说明服务器需要浏览器重定向到其他URL
。这时网络进程会从响应头的Location
字段里面读取重定向的地址,然后再发起新的HTTP
或者HTTPS
请求,一切又重头开始了。在导航过程中,如果服务器响应行的状态码包含了301
、302
一类的跳转信息,浏览器会跳转到新的地址继续导航 -
服务器返回的响应行的状态码是
200
,这是告诉浏览器一切正常,可以继续往下处理该请求了。
-
-
解析完响应行之后,浏览器通过
Content-Type
来决定如何显示响应体的内容。-
Content-type
字段的值是text/html
: 告诉浏览器,服务器返回的数据是HTML
格式,浏览器开始渲染 -
Content-Type
的值是application/octet-stream
: 显示数据是字节流类型的,通常情况下,浏览器会按照下载类型来处理该请求。
-
十三、如果资源类型为 HTML
, 解析 HTML
文档, 构建 DOM
树: 为该请求选择或者创建一个渲染进程, 渲染进程准备好之后, 网络进程和渲染进程之间会建立一个共享数据的管道, 网络进程接收到数据后就往这个管道里面放, 而渲染进程则从管道的另外一端不断地读取数据, 在渲染引擎内部, 有一个叫 HTML
解析器(HTMLParser
)的模块, 将读取的数据从 字节流 转换为 DOM
树。解析的过程中, 解析数据与网络进程加载数据同时进行,并不是等着整个 HTML
文档加载完成后在开始解析。
-
HTML
解析器 通过分词器将字节流转换为一个个Token
。解析后的Token
分为Tag Token
和文本 Token
,Tag Token
又分为StartTag
和EndTag
。ToKen
词语生成之后,随之被XSSAuditor
过滤。它会检测这些模块是否安全,比如是否引用了外部脚本, 是否符合CSP
规范,是否存在跨站点请求等。然后将Token
解析为DOM
节点, 并将DOM
节点添加到DOM
树中。HTML
解析器 维护了一个Token
栈结构,该Token
栈主要用来计算节点之间的父子关系,在第一个阶段中生成的Token
会被按照顺序压到这个栈中。通过 分词器 产生的新Token
就这样不停地压栈和出栈, 整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。 -
当
DOM
树 构建完成之后,浏览器触发DOMContentLoaded(onready)
事件,注册再该事件上的JavaScript
函数会被调用。当所有资源加载完之后,浏览器会触发onload
事件。
十四、HTML
解析器解析到 script
-
HTML
解析器解析到没有async
和defer
的script
时: 渲染引擎判断这是一段脚本。因为接下来的JavaScript
脚本可能要修改当前已经生成的DOM
结构,HTML
解析器暂停解析。所以JavaScript
代码的执行会阻碍后面DOM
节点 的创建同时也会阻碍后面的资源下载。如果遇到 外联脚本,JavaScript
执行 之前还需等待下载完成。-
在执行
JavaScript
之前,需要先解析JavaScript
语句之上所有的CSS
样式。所以如果代码里引用了外部的CSS
文件,那么在执行JavaScript
之前,还需要等待外部的CSS
文件下载完成,并解析生成CSSOM
对象之后,才能执行**JavaScript
** 脚本。而JavaScript
引擎 在解析JavaScript
之前,是不知道JavaScript
是否操纵了CSSOM
的,所以渲染引擎在遇到JavaScript
脚本时,不管该脚本是否操纵了CSSOM
,都会执行CSS
文件下载,解析操作,再执行JavaScript
脚本。 -
同样,在执行
JavaScript
之前,浏览器通过 预先扫描器 来扫描script
之后的词语,如果发现有其他资源,那么浏览器会通过 预资源加载器来发送请求,来提前下载资源。
-
-
HTML
解析器解析到有async
属性的script
时: 开始下载脚本并继续解析HTML
。脚本会在它下载完成后尽快执行, 但是解析器不会停下来等它下载。 -
HTML
解析器解析到有defer
属性的script
时: 开始下载脚本并继续解析HTML
。文档完全解析完成, 所有defer
脚本会按照在文档出现的顺序执行
十五、HTML
解析过程中的预解析: JavaScript
文件的 下载过程、执行过程 会阻塞 DOM
解析。而执行JavaScript
文件之前都会下载 Css
文件。而通常下载又是非常耗时的,会受到网络环境、JavaScript
和 Css
文件大小等因素的影响。Chrome
浏览器做了很多优化,其中一个主要的优化是 预解析操作。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML
文件中包含的 JavaScript
、CSS
等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。
十六、构建 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-index
做 z
轴排序等,为了更加方便地实现这些效果, 渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的 图层树(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
解析。从而阻塞页面的解析和渲染。
如果没有 defer
和 async
时, 将 <script>
放在 <head>
中, 浏览器会在渲染页面前加载并执行这些脚本,从而延迟首屏显示。如果将 <script>
放在页面底部(比如紧靠 </body>
标签之前),则页面的主要内容可以先加载并呈现出来,脚本在后续加载执行,不会阻塞首屏渲染。
如果有 async
属性时, 允许脚本异步加载, 但一旦加载完成会立即执行,仍可能中断渲染。
如果有 defer
属性时, 允许脚本异步加载, 且在 HTML
全部解析完毕后才执行,从而不阻塞页面渲染。
2.5 script
的加载会阻塞浏览器渲染吗?
默认情况下,浏览器在遇到 <script>
标签时,会暂停 HTML
的解析, 虽然浏览器会有预解析, 会提前加载 script
, 但是也需要等到 script
脚本执行完成后, 才继续 HTML
解析。从而阻塞页面的解析和渲染。
如果没有 defer
和 async
时, 将 <script>
放在 <head>
中, 浏览器会在渲染页面前加载并执行这些脚本,从而延迟首屏显示。如果将 <script>
放在页面底部(比如紧靠 </body>
标签之前),则页面的主要内容可以先加载并呈现出来,脚本在后续加载执行,不会阻塞首屏渲染。
如果有 async
属性时, 允许脚本异步加载, 但一旦加载完成会立即执行,仍可能中断渲染。
如果有 defer
属性时, 允许脚本异步加载, 且在 HTML
全部解析完毕后才执行,从而不阻塞页面渲染。
2.6 script
放在 head 里和放在 body 里有什么区别?
将 JavaScript
代码放在 <head>
标签内部和放在 <body>
标签内部有一些区别:
-
加载顺序:放在
<head>
里会在页面加载之前执行JavaScript
代码, 而放在<body>
里会在页面加载后执行。 -
页面渲染: 如果
JavaScript
代码影响了页面的布局或样式, 放在<head>
里可能会导致页面渲染延迟,而放在<body>
里可以减少这种影响。 -
代码依赖: 如果
JavaScript
代码依赖其他元素,放在<body>
里可以确保这些元素已经加载。 -
全局变量和函数: 放在
<head>
里的JavaScript
代码中的全局变量和函数在整个 页面生命周期内都可用。
如果没有 defer
和 async
时, 将 <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-image
是 CSS
属性,依赖于 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事件触发的先后顺序: 在一个典型的页面加载过程中,各事件的触发顺序大致如下:
-
DOM onready(DOMContentLoaded)
: 当HTML
解析完成、DOM
构建完毕时触发,此时不必等待图片、CSS
等加载完成。 -
图片
onready
(如果使用了类似检测图片尺寸的机制): 对于每个图片,当其元数据或尺寸信息就绪时(通常比文件完全加载早),会触发相应的回调。注意:这不是浏览器的标准事件,而是很多第三方库提供的功能。 -
图片
onload
: 当单个图片文件全部加载完成后触发。如果图片来自缓存,onload
可能会非常快触发,但原则上依然晚于 就绪 时机。 -
window onload
: 当页面中所有资源(包括所有图片、样式表、脚本等)都加载完成后,才会触发window onload
事件。