从输入URL到页面呈现发生了什么(5千多字!超级具体!)看完打通你的任通二脉,对前端的知识掌握的更好

77 阅读17分钟

从输入URL到页面呈现发生了什么

这张图就是从输入url到页面呈现的总过程,在这个过程主要经历了2个大流程,一个是网络流程,还有一个是渲染流程

Snipaste_2022-10-13_22-16-39

一.网络流程

1.构建请求行

浏览器会构建请求行:

// 请求方法是GET,路径为根路径,HTTP协议版本为1.1
GET / HTTP/1.1

2. 查找强缓存

先检查强缓存,如果命中直接使用,否则进入下一步。

3. DNS解析

由于我们输入的是域名,而数据包是通过IP地址传给对方的。因此我们需要得到域名对应的IP地址。这个过程需要依赖一个服务系统,这个系统将域名和 IP 一一映射,我们将这个系统就叫做DNS(域名系统)。得到具体 IP 的过程就是DNS解析

当然,值得注意的是,浏览器提供了DNS数据缓存功能。即如果一个域名已经解析过,那会把解析的结果缓存下来,下次处理直接走缓存,不需要经过 DNS解析

当然如果大家在优化github网速的时候,就有一个方法就是在host文件里面添加github实际的ip地址。

另外,如果不指定端口的话,默认采用对应的 IP 的 80 端口。

4. 建立 TCP 连接

建立 TCP连接经历了下面三个阶段:

  1. 通过三次握手(即总共发送3个数据包确认已经建立连接)建立客户端和服务器之间的连接。
  2. 进行数据传输。这里有一个重要的机制,就是接收方接收到数据包后必须要向发送方确认, 如果发送方没有接到这个确认的消息,就判定为数据包丢失,并重新发送该数据包。当然,发送的过程中还有一个优化策略,就是把大的数据包拆成一个个小包,依次传输到接收方,接收方按照这个小包的顺序把它们组装成完整数据包。
  3. 断开连接的阶段。数据传输完成,现在要断开连接了,通过四次挥手来断开连接。

看到这里,你应该明白 TCP 连接通过什么手段来保证数据传输的可靠性,一是三次握手确认连接,二是数据包校验保证数据到达接收方,三是通过四次挥手断开连接。

当然,如果再深入地问,比如为什么要三次握手,两次不行吗?第三次握手失败了怎么办?为什么要四次挥手等等这一系列的问题,大家就可以回去想想,这里就不多介绍了,我在这留下一个链接大家可以看看,这篇文章讲的比较深。 zhuanlan.zhihu.com/p/86426969

5.发送 HTTP 请求

现在TCP连接已经建立完毕,浏览器就可以和服务器开始通信,开始发送HTTP请求,然后HTTP请求一般包含3个东西:请求行请求头请求体

1.请求行:就是我们之前构建的请求

结构由请求方法请求URIHTTP版本协议组成。

// 请求方法是GET,路径为根路径,HTTP协议版本为1.1
GET / HTTP/1.1
2.请求头:告诉服务器的一些设置,和一些信息
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Cookie: /* 省略cookie信息 */
Host: www.baidu.com
Pragma: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1
3.请求体:一般只有在POST方法下存在,常见的场景是表单提交

6.服务端处理http请求

服务端在接收到请求时,内部会进行很多的处理

这里由于不是专业的后端分析,所以只是简单的介绍下,不深入

负载均衡

对于大型的项目,由于并发访问量很大,所以往往一台服务器是吃不消的,所以一般会有若干台服务器组成一个集群,然后配合反向代理实现负载均衡

当然了,负载均衡不止这一种实现方式,这里就不深入了...

简单的说:

用户发起的请求都指向调度服务器(反向代理服务器,譬如安装了nginx控制负载均衡),然后调度服务器根据实际的调度算法,分配不同的请求给对应集群中的服务器执行,然后调度器等待实际服务器的HTTP响应,并将它反馈给用户

后台的处理:

一般后台都是部署到容器中的,所以一般为:

  • 先是容器接受到请求(如tomcat容器)
  • 然后对应容器中的后台程序接收到请求(如java程序)
  • 然后就是后台会有自己的统一处理,处理完后响应响应结果

概括下:

  • 一般有的后端是有统一的验证的,如安全拦截,跨域验证
  • 如果这一步不符合规则,就直接返回了相应的http报文(如拒绝请求等)
  • 然后当验证通过后,才会进入实际的后台代码,此时是程序接收到请求,然后执行(譬如查询数据库,大量计算等等)
  • 等程序执行完毕后,就会返回一个http响应包(一般这一步也会经过多层封装)
  • 然后就是将这个包从后端发送到前端,完成交互

7.网络响应

HTTP 请求到达服务器,服务器进行对应的处理。最后要把数据传给浏览器,也就是返回网络响应。

跟请求部分类似,网络响应具有三个部分:响应行响应头响应体。和http请求报文很像

1.响应行

结构由HTTP版本协议状态码状态描述组成。

类似下面这样:

HTTP/1.1 200 OK

到这里网络流程就走完了,现在浏览器已经接收到了服务器给的信息了,那这些信息又是如何在页面上展示的呢,那么接下来就到我们渲染流程了。

在介绍渲染流程部分的时候,我感觉还是有必要去了解一下浏览器的架构和一些浏览器的基础知识

1.进程和线程

一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程

线程是依附于进程的,是CPU的最小的调度单元,而进程中使用多线程并行处理能提升运算效率。

2.浏览器的多进程架构

最新的Chrome浏览器包括:1个浏览器主进程、一个GPU进程、一个网络进程、多个渲染进程和多个插件进程。

下面我们来逐个分析下这几个进程的功能👇

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程( 最重要!)。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

不过凡事都有两面性,虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:

  • 更高的资源占用因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。
  • 更复杂的体系架构浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了

3.浏览器内核(渲染进程)

渲染进程又有5个线程: GUI渲染线程JavaScript引擎线程事件触发线程定时器触发器Http请求线程

GUI渲染线程:GUI 渲染线程负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。

JavaScript引擎线程: JavaScript 引擎线程主要负责解析 JavaScript 脚本并运行相关代码。 JavaScript 引擎在一个Tab页(Renderer 进程)中无论什么时候都只有一个 JavaScript 线程在运行 JavaScript 程序。需要提起一点就是,GUI线程与JavaScript引擎线程是互斥的,这也是就是为什么JavaScript操作时间过长,会造成页面渲染不连贯,导致页面出现阻塞的原理。

事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JavaScript 引擎的处理。 通常JavaScript引擎是单线程的,所以这些事件都会排队等待JS执行。

定时器触发器: 我们日常使用的setInterval 和 setTimeout 就在该线程中,原因可能就是:由于JS引擎是单线程的,如果处于阻塞线程状态就会影响记时的准确,所以需要通过单独的线程来记时并触发响应的事件这样子更为合理。

Http请求线程: 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求,这个线程就Http请求线程,它 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。

所以说到这里,你就应该知道js作为一个单线程语言却能写异步代码的原因了吧,而js事件循环机制(Event-Loop)也是在这些线程的基础上搭建出来的

现在一些前置概念,现在在来看渲染流程会有很大的帮助

二.渲染流程

1.HTML解析,构建DOM

由于浏览器无法直接理解HTML字符串,因此将这一系列的字节流转换为一种有意义并且方便操作的数据结构,这种数据结构就是DOM树DOM树本质上是一个以document为根节点的多叉树。

2.样式计算

关于CSS样式,它的来源一般是三种:

  1. link标签引用
  2. style标签中的样式
  3. 元素的内嵌style属性

然后拿到css的样式过后就可以开始真正的样式计算了

(1)格式化样式表

首先,浏览器是无法直接识别 CSS 样式文本的,因此渲染引擎接收到 CSS 文本之后第一件事情就是将其转化为一个结构化的对象,即styleSheets,也叫CSDOM。

这个格式化的过程过于复杂,而且对于不同的浏览器会有不同的优化策略,这里就不多说了。

在浏览器控制台能够通过document.styleSheets来查看这个最终的结构。当然,这个结构包含了以上三种CSS来源,为后面的样式操作提供了基础。

(2)标准化样式属性

有一些 CSS 样式的数值并不容易被渲染引擎所理解,因此需要在计算样式之前将它们标准化,如em->px,red->#ff0000,bold->700等等。

(3)计算每个节点的具体样式

3.生成布局树

现在已经生成了DOM树DOM样式,接下来要做的就是通过浏览器的布局系统确定元素的位置,也就是要生成一棵布局树(Layout Tree)。

布局树生成的大致工作如下:

  1. 遍历生成的 DOM 树节点,并把他们添加到布局树中
  2. 计算布局树节点的坐标位置。

值得注意的是,这棵布局树值包含可见元素,对于 head标签和设置了display: none的元素,将不会被放入其中。

然后生成布局树的细节也比较复杂,不过我们大多数情况下只需要知道它所做的工作是什么就好了,但是如果有兴趣也可以看看这个文章。 www.rrfed.com/2017/02/26/…

4.建图层树(分层)

如果你觉得现在DOM节点也有了,样式和位置信息也都有了,可以开始绘制页面了,那你就错了。

因为你考虑掉了另外一些复杂的场景,比如3D动画如何呈现出变换效果,当元素含有层叠上下文时如何控制显示和隐藏等等。

为了解决如上所述的问题,浏览器在构建完布局树之后,还会对特定的节点进行分层,构建一棵图层树(Layer Tree)。

那这棵图层树是根据什么来构建的呢?

一般情况下,节点的图层会默认属于父亲节点的图层(这些图层也称为合成层)。那什么时候会提升为一个单独的合成层呢?

有两种情况需要分别讨论,一种是显式合成,一种是隐式合成

显式合成

下面是显式合成的情况:

一、 拥有层叠上下文的节点。

层叠上下文也基本上是有一些特定的CSS属性创建的,一般有以下情况:

  1. HTML根元素本身就具有层叠上下文。
  2. 普通元素设置position不为static并且设置了z-index属性,会产生层叠上下文。
  3. 元素的 opacity 值不是 1
  4. 元素的 transform 值不是 none
  5. 元素的 filter 值不是 none
  6. 元素的 isolation 值是isolate
  7. will-change指定的属性值为上面任意一个。(will-change的作用后面会详细介绍)

二、需要剪裁的地方。

比如一个div,你只给他设置 100 * 100 像素的大小,而你在里面放了非常多的文字,那么超出的文字部分就需要被剪裁。当然如果出现了滚动条,那么滚动条会被单独提升为一个图层。

隐式合成

接下来是隐式合成,简单来说就是层叠等级低的节点被提升为单独的图层之后,那么所有层叠等级比它高的节点都会成为一个单独的图层。

这个隐式合成其实隐藏着巨大的风险,如果在一个大型应用中,当一个z-index比较低的元素被提升为单独图层之后,层叠在它上面的的元素统统都会被提升为单独的图层,可能会增加上千个图层,大大增加内存的压力,甚至直接让页面崩溃。这就是层爆炸的原理。这里有一个具体的例子

值得注意的是,当需要repaint时,只需要repaint本身,而不会影响到其他的层。

5.生成绘制列表

接下来渲染引擎会将图层的绘制拆分成一个个绘制指令,比如先画背景、再描绘边框......然后将这些指令按顺序组合成一个待绘制列表,相当于给后面的绘制操作做了一波计划。

6.生成图块和生成位图(栅格化操作)

在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口屏幕上页面的可见区域就叫视口),用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512,如下图所示:

然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。

通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。

因为GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。

7.合成和显示

而清楚这两个流程具体的过程。可以帮你了解前端性能优化的底层逻辑,JavaScript 运行机制解析和浏览器网络及安全机制解析,

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令DrawQuad,然后将该命令提交给浏览器进程。

浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。

一个完整的渲染流程大致可总结为如下:

  1. 渲染进程将 HTML 内容转换为能够读懂的 DOM树 结构。
  2. 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
  3. 创建布局树,并计算元素的布局信息。
  4. 对布局树进行分层,并生成分层树
  5. 为每个图层生成绘制列表,并将其提交到合成线程。
  6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  7. 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
  8. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

搞懂这个流程,你就知道为什么在优化 Web 性能的方法中,减少重绘、重排是一种很好的优化方式了

理解了这一整个流程,可以帮你把你零散的知识,又重新聚集到一起,这样记忆才深刻,也能明白各个名词的前因后果,从而打通任通二脉,知识在你脑子永远跑不了了。