前言
看标题就知道了,这一位真的是面试题老前辈了,请问哪个前端没经历过URL追问呢。有的是直接一顿输出八股文应试,有的是根据自己的想法表述有的可能直接就🤷♀️,今天我们旧嗑新唠,详细谈谈这中间都发生了些啥。
注:由于本篇着重对过程进行讲解,涉及到的HTTP、TCP/IP以及资源具体如何缓存、线程划分、事件循环机制等不做过多详解释。
地址解析
当我们在浏览器地址栏输入目标地址,后,浏览器首先解析输入的URL,确定其协议(如http或https)、主机名、端口(如果有指定)、路径和查询字符串。
DNS缓存检查
解析后也不是直接就去请求服务器,而是会搜索浏览器的DNS缓存,看是否存在对应的目标条目,如果有且没有过期则解析到此结束。注:查看浏览器DNS缓存命令(谷歌浏览器为例):chrome://net-internals/#dns
如果浏览器自身的缓存中没有查找到该条目,那么接下来就会搜索操作系统的DNS缓存。如果找到且没有过期则停止搜索解析结束
注:Windows操作系统DNS缓存命令:ipconfig /displaydns;macOS没有直接的查看命令,要查看具体的 DNS 查询,需要借助系统日志或第三方工具。
如果在操作系统的DNS缓存也没有找到,那么读取hosts文件,看此文件内有没有该域名对应的IP地址,如果有则解析成功。注:在 macOS/Linux 中,hosts文件通常是/etc/hosts ;在 Windows 中,它位于 C:\Windows\System32\drivers\etc\hosts
DNS解析
如果缓存中都没有主机名对应的IP地址,那我们又该如何查找到这台计算机/服务器然后进行连接呢?
在网络中定位是依靠IP进行身份定位的,所以我们可以通过服务器端的IP地址来进行定位。
不过实际我们很少见直接通过IP地址来访问,用户通常是使用主机名或域名来访问对方的计算机/服务器。因为与IP地址的一组数字相比,用字母配合数字的表示形式来指定计算机名更符合人类的记忆习惯,通俗一点就是URL地址好记,IP地址不好记。
这种方式方便我们记忆使用了,但让计算机去理解一串名称,相对而言就变得困难,因为计算机更擅长处理一串数字。因此,DNS服务应运而生,DNS协议提供通过域名查找IP地址,或逆向从IP地址反查域名的服务。
所以如果在hosts中也没有找到对应条目,那么浏览器就会发起一个DNS的系统调用,向DNS服务器发起查询请求,以获取该主机名对应的IP地址(这其中有域名解析的递归请求,在此不做过多解释)
资源缓存查找
之后,浏览器会检查是否有该资源的缓存副本(这是 HTTP 缓存的一部分,与 DNS 缓存无关)。
-
强缓存:浏览器检查资源的缓存头部(如
Cache-Control,Expires),看缓存是否有效。如果缓存有效且未过期,直接使用缓存资源,并不会发送 HTTP 请求。 -
协商缓存:如果强缓存失效或没有强缓存,浏览器会发送 HTTP 请求,附带缓存验证字段(如
If-Modified-Since,If-None-Match),询问服务器缓存是否有效。如果服务器返回304 Not Modified,浏览器使用本地缓存,否则接收新的资源并更新缓存。
强缓存存在问题及解决
如果服务器文件更新,但本地还是有缓存这样就会拿不到最新信息,我们可以使用一下方案解决(HTML页面一般不做强缓存,每一次html的请求都是正常的http请求):
- 服务器更新资源后,让资源名称和之前不一样,这样页面会导入全新的资源,例如使用webpack设置hash name
- 当文件更新后,我们在html导入的时候,设置一个后缀(时间戳),例如
<script src=“index.js?1111”></script><script src=“index.js?2323”></script>
强缓存和协商缓存区别
协商缓存总会和服务器协商,所以一定要发http请求的
TCP三次握手
拿到域名对应的IP地址后,客户端会向服务器发起TCP连接请求,这其中涉及到一个“三次握手”的请求,“三次握手”就是为了验证客户端的发送能力和接收能力以及服务器端的发送能力和接收能力。
HTTP请求/数据传输
一旦TCP连接建立,浏览器会构造一个HTTP请求消息,包括请求行(方法、URL、HTTP版本)、请求头和请求体(如果有的话),然后将这个请求发送到服务器。
服务器端拿到了客户端的请求参数之后,会进行相应的业务处理,处理完成之后,再将处理的结果返回给客户端。
TCP四次挥手
在经过一次请求和一次响应之后,客户端和服务器的“交流”就结束了,此时就可以执行 TCP 连接断开的流程了,它需要 "四次挥手"
-
客户端发送 FIN(Finish)报文
数据传输完成后,客户端会发送一个FIN报文,告诉服务器它要关闭连接。此时,客户端进入FIN_WAIT_1状态。
-
服务器发送 ACK 报文
服务器收到客户端的FIN报文后,确认已经收到,并发送一个ACK报文回应,表示它也知道客户端要关闭连接。此时,客户端进入FIN_WAIT_2状态,服务器进入CLOSE_WAIT状态。
-
服务器发送 FIN 报文
服务器在发送ACK报文后,可能还需要处理一些未完成的任务。一旦服务器完成所有任务,准备关闭连接时,它会发送一个FIN报文给客户端。此时,服务器进入LAST_ACK状态。
-
客户端发送 ACK 报文
客户端收到服务器发送的FIN报文后,会回应一个ACK报文,表示确认关闭连接。此时,客户端进入TIME_WAIT状态,以防止服务器未收到ACK并重新发送FIN。经过一定的时间(通常是2倍的MSL,最长报文段寿命),客户端会彻底关闭连接,进入CLOSED状态。
另外也许有人会问,如果每次请求都要重新建立连接,这样不会造成额外开销吗?如果我们希望在加载同一个网页中的内容时,尽量复用连接而不每次都重连,那么可以使用 Connection: keep-alive。当启用 HTTP Keep-Alive 时,浏览器会复用同一个 TCP 连接来请求多个资源,从而减少重新建立连接的开销。这个连接在所有资源传输完成后,可能会保持一段时间,以等待是否还有后续请求。如果在这段时间内没有新请求发生,TCP 连接才会关闭,触发四次挥手。HTTP/1.0 需要手动设置 keep-alive,而 HTTP/1.1 默认启用这个特性。
然而,HTTP/1.1 中的连接复用是串行化的,即使连接是持久的,后续请求必须等待前一个请求的响应完成才能开始。这可能会导致“队头阻塞”,即一个请求的延迟会影响后续请求的处理。
相较之下,HTTP/2 中的多路复用机制更为高效。HTTP/2 允许在同一个 TCP 连接中并发处理多个请求和响应,使用流的方式独立发送和接收数据,从而避免了队头阻塞问题。由于多路复用机制,HTTP/2 不再依赖 keep-alive 来处理多个请求,因此大大提升了传输效率。
页面渲染
一般我们访问网站服务器都会返回给我们一个html文件,通常叫做index.html文件,为什么会这样呢?index.html 被称为入口文件,是网站默认的起点文件,当用户访问目录时,Web 服务器会自动返回该文件。另外index.html 负责为浏览器提供初始页面结构,并引导其加载其他资源,是整个页面渲染的起点。
不过我们拿到了这个文件,为什么浏览器是拿什么去渲染和解析这个代码的呢?这就涉及到浏览器内核。它是浏览器自己开发出来的,用于解析html和css代码,并渲染成用户所看到的页面的工具。学术名词是Rendering Engin,也被叫做渲染引擎或者排版引擎。负责解析html和css的有了那么负责解析js的呢?这就需要用到JS引擎,它是专门用来渲染解析JavaScript代码的(早期融合在浏览器内核中,现在基本上都是分离开的)。
简单介绍下浏览器的内核以及引擎(标的是新版本现在使用的,之前版本的没有标):
| 谷歌 | Firefox火狐 | 微软Edge | IE | Opera欧朋 | Safari | |
|---|---|---|---|---|---|---|
| 浏览器内核 | Blink | Gecko | Blink | Trident | Blink | Webkit |
| JS引擎 | V8 | JaegerMonkey | Chakra | Chakra | Carakan | Nitro |
下面言归正传,说回浏览器的渲染过程:
解析HTML,构建DOM树
- 获取HTML文件。浏览器接收返回的HTML文档,进行解析。
- HTML标记识别。浏览器会将HTML文件解析成一个个标记,如div、p、img等。解析过程中,浏览器会忽略一些不合法的标记,如没有闭合标签、属性值没有使用引号等。
- DOM树构建。浏览器会将解析后的标记转化成一个个DOM节点(Node),构建成一棵 DOM 树(Document Object Model)。DOM 树是一个树形结构,根节点是 document,其他节点代表 HTML 文档中的元素、属性、文本等。
在构建 DOM 树的过程中,浏览器会按照 HTML 文档的层次结构,将文档分成一个个的块(block),如文本块、段落块、表格块等等。每个块都会被转换成一个 DOM 节点,节点之间的关系由 HTML 标记之间的关系来确定。
解析CSS,构建CSSOM树
解析过程中遇到CSS解析CSS,遇到JS执行JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载HTML中的外部CSS文件和外部的JS文件以便加快后续的渲染速度。
如果解析到link位置,此时的外部CSS文件还没有下载解析好,线程不会等待,而是继续解析后续的HTML。这是因为下载和解析CSS的工作是在预解析线程中进行(这就是CSS不会阻塞HTML解析的根本原因)。
如果解析到script位置,会停止解析HTML,转而等待JS文件下载好,并将全局代码解析执行完成后,才能继续解析HTML,这是因为JS代码的执行过程中可能会修改当前的DOM树,所以DOM树的生成必须暂停,这是JS会阻塞HTML解析的根本原因。这一部分的优化可以通过async或defer属性来实现。
这一步完成后,会得到DOM树和CSSOM树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在CSSOM树中。
将DOM树和CSSOM树合并成渲染树
当有了DOM Tree和 CSSOM Tree后,就可以两个结合来构建Render Tree了
生成布局树,计算每个元素在页面上的位置和大小
接下来是布局,布局完成后会得到布局树。布局阶段会依次遍历DOM树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。 大部分时候DOM树和布局树并非一一对应,例如display:none的节点没有几何信息,因此不会生成布局树,又比如使用了伪元素选择器,虽然DOM树中不存在这些伪元素节点,但是它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致DOM树和布局树无法一一对应
第一次确定每个节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为重排。
分层
主线程会使用一套复杂的策略对整个布局树进行分层 分层的好处在于将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。滚动条、堆叠上下文、transform、opacity等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果
分层确实可以提高性能,但在内存管理方面成本较高,因此不应作为 Web 性能优化策略的过度使用。
绘制
主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。
合成
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作由合成线程完成。合成线程首先对每个涂层进行分块,将其划分为更多的小区域,它会从线程池中拿取多个线程来完成这份工作。
光栅化
分块完成后进入光栅化阶段。合成线程会将块信息交给GPU线程,以极高的速度完成光栅化。GPU线程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。光栅化的结果,就是一块一块的位图
画,生成像素信息
合成线程拿到每个层、每个块的位图后,生成一个个的quad信息。这些信息描述了在屏幕上如何渲染和组合不同的图层,会考虑到旋转、缩放等。变形发生在合成线程,与主线程无关,这就是transform效率高的本质原因。合成线程会把quad提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕成像。
另外,这里还有两个比较重要的概念:回流和重绘
reflow回流: reflow的本质就是重新计算layout树。当进行了会影响布局树的操作后,需要重新计算布局树,会引发layout,例如改变元素的大小、位置或布局属性。为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当js代码全部完成后再进行统一计算。所以,改动属性造成的reflow是异步完成的。 也同样因为如此,当js获取布局属性时,就可能造成无法获取到最新的布局信息。浏览器在反复权衡下,最终决定获取属性立即reflow
repaint重绘:repaint的本质就是重新根据分层信息计算了绘制指令。当改动了可见样式后,就需要重新计算,会引发repaint,例如修改元素的颜色、背景色等。由于元素的布局信息也属于可见样式,所以reflow一定会引起repaint
从这些过程中我们也可以找到一些性能优化的切入点,例如:缓存、DNS优化、Connection:keep-alive、减少HTTP请求次数及数据传输大小......
好了以上就是“输入URL地址到页面呈现”此过程的相关内容了,大家如果觉得有所帮助可以给个赞。有不同观点也欢迎评论指出
撒花🎉🎉🎉