【前端】浏览器输入url到页面呈现发生了什么?
前言
在此总结记录下浏览器输入url到页面呈现期间的流程。
浏览器输入url发生了什么?
从浏览器地址栏上输入url到页面渲染主要分为以下流程:
- 解析请求url,建立连接
- 发送请求
- 浏览器渲染页面
在输入url地址后,浏览器识别输入的是url后会根据url的http类型请求这个url,首先进行缓存查找,若没有则会使用DNS进行域名解析,然后根据IP地址找到服务器,建立TCP链接,然后发送请求,服务器接收请求处理后将返回资源。
浏览器拿到服务器反的html资源后会进行解析渲染,包括html所需的css资源和js资源等也进行解析和处理,进行合并就算、布局、合成,以及渲染等步骤呈现页面内容。
解析请求url
URL(统一资源定位符)是因特网中的唯一资源的地址。它是浏览器用于检索已发布资源(例如 HTML 页面、CSS 文档、图像等)的关键机制之一。
url通常包含url协议、域名(IP)、端口、路径、路径参数和锚点。
如果 url中使用的是域名,则会DNS解析(基于UDP)查找到IP地址。浏览器向域名服务器发起 DNS 查询请求,最终得到一个 IP 地址。第一次请求之后,这个 IP 地址可能会被缓存一段时间,这样可以通过从缓存里面检索 IP 地址而不是再通过域名服务器进行查询来加速后续的请求。
然后根据IP地址找到服务器,创建TCP链接。
创建TCP链接总共经历三次握手:
- 浏览器发送一个带SYN标记的tcp包给服务器,表示请求建立连接。
- 服务器同意后会发送一个带SYN-ACK的确认包给浏览器,表示同意建立连接。
- 浏览器收到SYN-ACK确认包后,再向服务器发送一个确认包(ACK),表示连接已经建立。
TCP 的“三次握手”技术经常被称为“SYN-SYN-ACK”——更确切的说是 SYN、SYN-ACK、ACK——因为通过 TCP 首先发送了三个消息进行协商,然后在两台电脑之间开始一个 TCP 会话。是的,这意味着当请求尚未发出的时候,终端与每台服务器之间还要来回多发送三条消息。
对于通过 HTTPS 建立的安全连接,还需要进行TLS协商。协商决定使用哪种密码对通信进行加密,验证服务器,并在开始实际数据传输前建立安全连接。这就需要在实际发送内容请求之前,再往返服务器五次。
虽然建立安全连接的步骤增加了等待加载页面的时间,但是为了建立一个安全的连接而增加延迟是值得的,因为在浏览器和 web 服务器之间传输的数据不可以被第三方解密。
如此经过 8 次往返,浏览器终于可以发出请求。
发送请求
一旦我们建立了和 web 服务器的连接,浏览器就会代表用户发送一个初始的 HTTP GET 请求,对于网站来说,这个请求通常是一个 HTML 文件。一旦服务器收到请求,它将使用相关的响应头和 HTML 的内容进行回复。
超文本传输协议(HTTP)是一个用于传输超媒体文档(例如 HTML)的应用层协议。它是为 Web 浏览器与 Web 服务器之间的通信而设计的,但也可以用于其他目的。HTTP 遵循经典的客户端—服务端模型,客户端打开一个连接以发出请求,然后等待直到收到服务器端响应。HTTP 是无状态协议,这意味着服务器不会在两个请求之间保留任何数据(状态)。
如果http响应状态码以3开头表示重定向,则会使用响应头的location的值进行再次请求。
浏览器渲染页面
浏览器拿到数据后,就立马进行解析。
HTML 和CSS
-
浏览器解析HTML,得到DOM树。(document)
-
浏览器解析CSS,得到CSSOM对象。(document.styleSeets)
-
DOM和CSSOM进行合并计算出每个节点的具体样式,得到渲染树。(Attachment)
-
布局步骤,在渲染树上运行布局以计算每个节点的几何体。布局是确定呈现树中所有节点的尺寸和位置,以及确定页面上每个对象的大小和位置的过程。
重排是后续过程中对页面的任意部分或整个文档的大小和位置的重新计算。
-
通过布局树,进行分层步骤(定位、透明属性、transform属性、clip等)生成图层树。
-
将不同图层栅格化进行绘制,转交给合成线程处理,最后呈现页面内容。此时
重绘会触发重新绘制和重新合成。
回流(Reflow): 当页面布局和几何属性发生变化,导致部分或全部元素的尺寸、位置、结构发生改变时,浏览器需要重新计算元素的几何属性和页面的布局,这个过程被称为回流。
在第一步解析DOM时,当解析器发现非阻塞资源,例如一张图片,浏览器会请求这些资源并且继续解析。当遇到一个 CSS 文件时,解析也可以继续进行,但是对于 <script> 标签(特别是没有 async 或者 defer 属性的)会阻塞渲染并停止 HTML 的解析。
在解析 CSS 和创建 CSSOM 的同时,包括 JavaScript 文件在内的其他资源也在下载(这要归功于预加载扫描器)。JavaScript 会被解析、编译和解释。脚本被解析为抽象语法树。有些浏览器引擎会将抽象语法树输入编译器,输出字节码。这就是所谓的 JavaScript 编译。大部分代码都是在主线程上解释的,但也有例外,例如在 web worker 中的代码将会在独立线程运行。
在合成渲染树阶段,不会被显示的元素,如 <head> 元素及其子元素,以及任何带有 display: none 的节点,如用户代理样式表中的 script { display: none; },都不会包含在渲染树中,因为它们不会出现在渲染输出中。应用了 visibility: hidden 的节点会包含在渲染树中,因为它们会占用空间。
JavaScript
在解析到js时,会进行并行下载js文件,停止渲染,下载完成后使用v8串行执行js文件。
js的执行过程一定是会阻塞Dom Tree和Css OM的。有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。
v8执行js代码流程:
- 通过词法分析和语法分析生成抽象语法树(AST),有了 AST 后,那接下来 V8 就会生成该段代码的执行上下文。
- 解释器 Ignition生成字节码。
- 字节码通过即时编译(JIT)技术生成机器码执行,
即时编译(JIT,就是指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了(经常使用)之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
JavaScript 是一种基于原型、多范式、单线程的动态语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。
js在执行I/O操作时为了不阻塞主线程,采用一种事件循环的异步编程机制。
其他
HTTP请求类型
HTTP0.9 - 请求由单行指令构成,以唯一可用方法 GET 开头,其后跟目标资源的路径(一旦连接到服务器,协议、服务器、端口号这些都不是必须的)。
HTTP1.0 - 开始有请求头概念,默认模型是短连接。
HTTP1.1- 默认是长连接,即开启了keep-alive链路复用;增加管线化技术,允许在第一个应答被完全发送之前就发送第二个请求,以降低通信延迟;以及支持响应分块、支持缓存控制机制等。
HTTP/2 - HTTP/2 是二进制协议而不是文本协议。这是一个多路复用协议(一个域名一个TCP连接),解决了HTTP的队头阻塞问题。并行的请求能在同一个链接中处理,移除了 HTTP/1.x 中顺序和阻塞的约束。压缩了标头。因为标头在一系列请求中常常是相似的,其移除了重复和传输重复数据的成本。其允许服务器在客户端缓存中填充数据,通过一个叫服务器推送的机制来提前请求。
HTTP/3 - HTTP/3 有着与 HTTP 早期版本的相同语义,但在传输层部分使用 QUIC 而不是 TCP。QUIC 旨在为 HTTP 连接设计更低的延迟。类似于 HTTP/2,它是一个多路复用协议,通过 UDP 运行多个流,并为每个流独立实现数据包丢失检测和重传,因此如果发生错误,只有该数据包中包含数据的流才会被阻止。
浏览器的多进程模型
浏览器的多进程模型提高了浏览器的稳定性和安全性,缺点是内存消耗高。
进程是操作系统资源分配的基本单位 ,进程中包含线程。
- 浏览器进程:负责界面显示、用户交互、子进程管理、提供存储等。
- 渲染进程:每个窗口都会有一个渲染进程,用于显示页面。它包括GUI线程(渲染页面)、js线程(v8引擎)、定时触发器线程(定时器计数)、事件触发线程、异步http请求线程。
- 网络进程: 处理网络资源加载(html、css、js等)。
- GPU进程:负责处理整个应用程序的GPU任务。
- 插件进程:浏览器的插件使用。
事件循环
事件循环由浏览器内部的"任务队列"管理,主要包括宏任务(Macro Task)和微任务(Micro Task)。
宏任务包括:
- 整体的脚本(script)
- setTimeout
- setInterval
- I/O操作
- setImmediate(只在IE中有效)
微任务包括:
- process.nextTick(Node.js中有效)
- Promise
- async/await
- Object.observe(已废弃)
- MutationObserver
事件循环流程 :
- 主线程执行第一个宏任务 。
- 遇到同步操作直接执行并弹出栈。
- 遇到异步操作时,交给宿主环境异步执行,执行成功后将回调函数推入任务队列。若执行的是宏任务则推入宏任务队列,否则推入微任务队列。
- 当调用栈执行完宏任务的所有代码,将微任务队列中的代码依次推入调用栈执行,直到微任务执行完毕被清空。
- 将下一个宏任务推入调用栈执行。
- 以上过程会不断重复,直到宏任务队列全部执行完毕。