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

马丁路德
• 阅读 1557

这个问题是前端的经典问题,从这个问题出发我们可以从根本上了解如何解决性能优化问题~

首先我们可以在开头大概了解下在浏览器输入 URL到页面展示,中间有哪些步骤:

  • 用户从浏览器进程里输入请求信息
  • 网络发起 URL 请求
  • 服务器响应 URL 请求之后,浏览器进程就要开始准备渲染进程了
  • 渲染进程准备好之后,需要先向渲染进程提交页面数据,我们称之为提交文档的阶段
  • 渲染进程接收完文档信息之后,开始解析页面和加载子资源,完成页面的渲染

接下来我们详细的了解下各个步骤干了些什么

1. 用户输入

首先用户在地址栏输入一个查询关键字的时候,地址栏会判断输入的是搜索内容还是请求 URL

  • 如果是搜索内容,地址栏会使用浏览器的默认搜索引擎,来合成新的带搜索关键字的 URL
  • 如果输入的内容符合 URL 的规则,比如输入的是 baidu.com ,那么地址栏会根据规则,把这段内容加上协议,合成完整的 URL,比如:www.baidu.com

当用户按下回车,浏览器开始加载 URL

2. URL 请求过程

这一步开始进入页面资源请求过程,首先网络进程会查找本地缓存是否缓存了该资源

  • 如果缓存了该资源,那么直接返回资源给浏览器进程
  • 如果在缓存中没有,直接进入网络请求流程,请求前第一步是进行 DNS 解析,以获取请求域名的服务器 IP 地址,如果进行的 HTTPS 请求协议,那么还需要建立 TLS 连接

接下来利用 IP 地址和服务器简历 TCP 连接,连接建立之后,浏览器会构建请求行、请求头等信息,并把该域名相关的 cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息

服务器收到请求信息后,会根据请求信息生成响应数据(包括响应行,响应头,响应体)等信息, 并发送给网络进程,等网络进程接受了响应行和响应头后,就开始解析响应头的内容。

在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是 301 或者 302,那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。

直到服务器的响应头状态码是 200,就可以继续往下处理该请求了

接下来就是对响应数据的类型处理,因为 URL 请求的数据类型,有时候是下载类型,有时候是正常的 HTML 页面,浏览器如何区分它们呢?

答案是 Content-Type,这个字段告诉浏览器服务器返回的是什么类型的响应体,然后浏览器根据 Content-Type 的值来决定如何显示响应体的内容。

比如:

  • Content-Type: text/html - 服务器的返回数据格式是 HTML 格式
  • Content-Type: application/octet-stream - 显示数据是字节流类型,通常情况下浏览器会按照下载类型来处理该请求

所以如果 Content-Type 的值被浏览器判断成下载类型,那么该请求会被提交给浏览器的下载管理器,同时 URL 请求的导航流程就此结束

如果是 HTML ,浏览器会继续进行导航流程,由于 Chrome 的页面渲染是运行在渲染流程中的,所以接下来就要准备渲染进程了

3. 准备渲染进程

通常情况下,Chrome 会为每个页面分配一个渲染进程,但是对同一站点的情况就会共用一个进程(之前讲过)

渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就到了提交文档阶段。

4. 提交文档

这里的文档,指的是 URL 请求的响应体数据

  • 提交文档的消息是由浏览器进程发出的,渲染进程接收到提交文档的消息后,会和网络进程建立传输数据的通道
  • 等文档数据传输完成之后,渲染进程会返回确认提交的消息给浏览器进程
  • 浏览器接收到确认提交的消息后,才会更新浏览器界面状态,包括了安全状态、地址栏状态的 URL、前进后退的历史状态,并更新 Web 页面

这也就解释了为什么在浏览器输入一个地址后,之前的页面没有马上消失,二是要加载一会才回更新页面。 到了这里,一个完整的导航流程就走完了,马上进入渲染阶段

5. 渲染阶段

一旦文档被提交,渲染进程就要开始页面解析和子资源加载了,这就进入到了渲染流程

我们知道页面的三大要素:

  • HTML - 由标签和文本组成,负责页面的结构显示
  • CSS - 层叠样式,由选择器和样式组成 - 负责文本的样式
  • JavaScript - 负责页面的逻辑交互

由于渲染机制过于复杂,所以渲染模块在执行过程中会被划分为很多子阶段,输入的 HTML\CSS\JavaScript 经过这些子阶段,最后输出像素,这样的处理流程叫做渲染流水线,大致如图所示

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

按照渲染的时间顺序,流水线可以氛围如下几个子阶段:

构建 DOM 树 ---> 样式计算 ---> 布局阶段 ---> 分层 ---> 绘制 ---> 分块 ---> 光栅化 ---> 合成

在每个阶段都会有三个内容:

  • 开始每个子阶段都有其输入的内容
  • 每个子阶段都有其处理过程
  • 最终每个子阶段都会生成输出内容

那么接下来我们一步步来了解这些子阶段

构建 DOM 树

由于浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器可以理解的解构 - DOM 树 树结构如下所示,其中的每个点我们叫做节点

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

我们可以在开发者工具的控制台中,输入 document 得到该网页的完整 DOM 结构, 如下图所示

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

DOM 和 HTML 内容几乎是一样的,但是和 HTML 不同的是,DOM 是保存在内存中的树状结构,可以通过 JavaScript 来查询或者修改其内容

所以构建 DOM 的输入内容是一个 HTML 文件,经由 HTML 解析器解析,最终输出树状结构的 DOM

这样已经生成好 DOM 树了,但是 DOM 节点的样式还未知,所以下一步就是样式计算

样式计算

我们知道 CSS 样式来源有三种:

  • link 引用的外部 CSS
  • 标记内的 CSS
  • 元素的 style 属性内嵌的 CSS

和 HTML 文件一样,浏览器并不能直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本的时候,会执行一个转换操作,把 CSS 转换成浏览器可以理解的:styleSheets

在控制台中,输入 document.styleSheets 得到该网页的 styleSheets ,如下图所示

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

这个图里面就包含了我们刚刚说到的三种样式来源 接下来浏览器就会把样式表中的属性进行标准化转换(例如 em/rem -> px)

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

转换示例如上图所示

转换完成之后,就下来就需要计算 DOM 树种每个节点的样式属性了,那么如何来进行计算呢?

这就涉及到了 CSS 的继承规则和层叠规则

  • CSS 继承
    • CSS 继承就是每个节点都会包含父节点有的样式(比如 body 节点设置了 font-size:18px; 那么 body 下面的子节点的 font-size 都是18)
    • 在开发者工具控制台的 computed 中,我们可以查看样式来源,可以看到具体的某些样式是来自样式文件还是 UserAgent 样式表(浏览器的默认样式)
  • 层叠样式规则
    • 层叠是 CSS 的一个基本特征,它定义了如何合并来自多个源的属性值的算法

样式计算阶段的目的是为了计算出 DOM 节点中每个元素的具体样式,在计算过程就需要遵循 CSS 继承和层叠两个规则

这个阶段最终输出的是每个 DOM 节点的样式,并且保存在 ComputedStyle 的结构中。

布局阶段

到了这个阶段,我们有了 DOM 树和 DOM 数中元素的样式,但是呢这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息

那么接下来就要计算出 DOM 树中可见元素的几何位置,这个计算过程叫做布局

1. 创建布局树

在 DOM 树中可能会有很多不可见的元素,比如 head 标签和一些有 display:none 的属性,所以在显示前,我们还要额外构建一棵只包含可见元素的布局树 我们可以结合下图来看看布局树的构造过程:

在浏览器输入 URL到页面展示中间发生了什么? 从上图可以看出来,DOM 树种所有不可见的元素都没有包含到布局树中

为了构建布局树,浏览器大体上完成了下面这些工作

  • 遍历DOM 树中所有可见节点,并把这些节点加到布局中
  • 不可见的节点会被忽略掉,如 head 标签下面的全部内容,以及 display:none 的元素也不会被包含进布局树

2. 布局计算

现在我们有了一棵完整的布局树,接下来就是计算布局树中节点的坐标位置(计算过程暂时略过,比较复杂)

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

分层

有了布局树之后,是不是就可以开始绘制页面了呢?答案当然是 false 因为页面中往往有很多复杂的效果,比如 3D 变换,页面滚动、z-index 的 Z 轴排序等等,为了更方便的实现这些效果 渲染引擎还需要为特定的节点生成专用的涂层,并生成一棵对应的图层树,正是各种各样的图层叠加在一起构成了最终的页面

打开 Chrome 的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况,

到这里我们就知道浏览器的页面实际上被分成了很多图层,这些图层叠加合成了最终的页面,我们可以来看看这些图层和布局树节点之间的关系:

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

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的图层,那么这个节点就舒服父节点的图层。

不管怎么样,最种每个节点都会直接或间接的属于一个层。

那么这里有个问题,在什么样的条件下,渲染引擎才会为特定的节点创建新的图层呢?

  • 拥有层叠上下文属性的元素会被提升为单独的一层(position:fixed / z-index / filter:blue(5px) / opacity: 0.5 等)
  • 需要剪裁 (clip) 的地方也会被单独创建图层(比如一个 div 长宽只有 200,但是内容很多,那么多余的部分就会出现被裁剪,这种情况渲染引擎会为文字部分单独创建一层,如果出现滚动条,那么滚动条也是一个单独的层)

所以如果元素有了层叠上下文属性或者需要被剪裁,满足任意一点就会被提升为单独的图层

图层绘制

完成了图层树的创建之后,渲染引擎会对图层树中的每个图层进行绘制(按照基本回话顺序,比如先底色再画图形的顺序,所以是低层级到高层级的绘制) 我们可以打开开发者工具的 Layers 标签,选择 document 层,就可以实际体验下绘制列表。

栅格化

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。 我们可以结合下图来看下渲染主线程和合成线程之间的关系:

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

当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图

合成线程发送绘制图块命令DrawQuad给浏览器进程,浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上

总结

  • 浏览器不能直接理解 HTML 数据,所以第一步需要将其转换为浏览器能够理解的 DOM 树结构;
  • 生成 DOM 树后,还需要根据 CSS 样式表,来计算出 DOM 树所有节点的样式;
  • 最后计算 DOM 元素的布局信息,使其都保存在布局树中。

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

不会阻塞dom树的构建,原因Html转化为dom树的过程,发现文件请求会交给网络进程去请求对应文件,渲染进程继续解析Html。 会阻塞页面的显示,当计算样式的时候需要等待css文件的资源进行层叠样式。资源阻塞了,会进行等待,直到网络超时,network直接报出相应错误,渲染进程继续层叠样式计算

6. 渲染完成

页面生成完成后,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后,会停止标签图标上的加载动画。这样,一个完整的页面就生成了。

本文转自 https://juejin.cn/post/6939723201597407263,如有侵权,请联系删除。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
2年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
2个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这