从输入网址到页面呈现这个过程中都发生了什么?

在前端开发中我们常常需要考虑首屏加载时间,为了尽可能减少首屏加载时间我们需要弄清楚从输入网址到页面最终呈现的过程中都发生了哪些事情,然后才能具体问题具体分析,最终达到提升网页性能的目的。从输入网址到页面呈现过程中都发生了什么?据说这是一个非常经典的面试题,考察的问题面也很广,今天我就从一个前端开发工程师的角度来解答一下这个问题,文中难免有些知识点介绍的不够深,还望见谅!

从输入网址到页面呈现这个过程大致可分为以下这几个部分:

  1. 网络通信
  2. 页面渲染

网络通信

输入网址

当我们在浏览器的地址栏输入网址例如(http://www.baidu.com),http://代表使用超文本传输协议,www.baidu.com代表服务器地址,baidu.com代表域名。一个完整的 URL 包括协议、服务器地址(主机)、端口、路径

负责域名查询与解析的 DNS 服务

用户通常使用主机名或域名来访问某网站,而不是直接通过 IP 来访问,因为字母数字配合的表示形式更符合人类的记忆习惯,可计算机却不理解这些名称,因此 DNS 服务应运而生,DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。
DNS 查询过程如下:

  1. 操作系统会先检查本地的 hosts 文件是否有这个网址映射关系,如果有,就先调用这个 IP 地址映射,完成域名解析。
  2. 如果 hosts 里没有这个域名的映射,则查找本地 DNS 解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
  3. 如果 hosts 与本地 DNS 解析器缓存都没有相应的网址映射关系,首先会找 TCP/IP 参数中设置的首选 DNS 服务器,在此我们叫它本地 DNS 服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
  4. 如果要查询的域名,不由本地 DNS 服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个 IP 地址映射,完成域名解析,此解析不具有权威性。
  5. 如果本地 DNS 服务器本地区域文件与缓存解析都失效,则根据本地 DNS 服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地 DNS 就把请求发至 13 台根 DNS,根 DNS 服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个 IP。本地 DNS 服务器收到 IP 信息后,将会联系负责.com 域的这台服务器。这台负责.com 域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com 域的下一级 DNS 服务器地址(baidu.com)给本地 DNS 服务器。当本地 DNS 服务器收到这个地址后,就会找 baidu.com 域服务器,重复上面的动作,进行查询,直至找到www.baidu.com主机。
  6. 如果用的是转发模式,此 DNS 服务器就会把请求转发至上一级 DNS 服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根 DNS 或把转请求转至上上级,以此循环。不管是本地 DNS 服务器用是是转发,还是根提示,最后都是把结果返回给本地 DNS 服务器,由此 DNS 服务器再返回给客户机。

从客户端到本地 DNS 服务器是属于递归查询,而 DNS 服务器之间就是的交互查询就是迭代查询。

应用层 客户端发送 HTTP 请求报文

HTTP 报文包括:

  • 报文首部 (请求行+各种首部字段+其他)
  • 空行
  • 报文主体 (应被发送的数据)通常并不一定要有报文主体

下面对百度首页请求报文首部进行分析:
请求行

请求方法GET 请求URI /   HTTP协议版本 1.1
GET / HTTP/1.1

首部字段

请求资源所在服务器
Host: www.baidu.com
连接方式:持久连接     HTTP/1.1之前版本默认非持久连接
Connection: keep-alive
报文指令:要求所有中间服务器不返回缓存资源
Pragma: no-cache
控制缓存的行为:缓存前必须先确认其有效性,防止从缓存中返回过期的资源
Cache-Control: no-cache
用户代理可处理的媒体类型
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8   q表示权重从而区分优先级
http客户端浏览器信息
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36
可接受的内容编码类型
Accept-Encoding: gzip, deflate, sdch
可接受的语言
Accept-Language: zh-CN,zh;q=0.8
相关信息或标记
Cookie: BAIDUID=3C67AA3EF6B3347D3AA986CE489268C4:FG=1; BIDUPSID=3C67AA3EF6B3347D3AA986CE489268C4;

传输层 确保传输报文可靠性的 TCP 协议

位于传输层的 TCP 协议为传输报文提供可靠的字节流服务。为了方便传输,将大块的数据分割成以报文段为单位的数据包进行管理,并为它们编号,方便服务器接收时能准确地还原报文信息。TCP 协议通过“三次握手”等方法保证传输的安全可靠。
“三次握手”的过程是,发送端先发送一个带有 SYN(synchronize)标志的数据包给接收端,在一定的延迟时间内等待接收的回复。接收端收到数据包后,传回一个带有 SYN/ACK 标志的数据包以示传达确认信息。接收方收到后再发送一个带有 ACK 标志的数据包给接收端以示握手成功。在这个过程中,如果发送端在规定延迟时间内没有收到回复则默认接收方没有收到请求,而再次发送,直到收到回复为止。
详细过程如下图
三次握手

网络层 负责传输的 IP 协议

IP 协议的作用是把 TCP 分割好的各种数据包传送给接收方。而要保证确实能传到接收方还需要接收方的 MAC 地址,也就是物理地址。IP 地址和 MAC 地址是一一对应的关系,一个网络设备的 IP 地址可以更换,但是 MAC 地址一般是固定不变的。ARP 协议可以将 IP 地址解析成对应的 MAC 地址。当通信的双方不在同一个局域网时,需要多次中转才能到达最终的目标,在中转的过程中需要通过下一个中转站的 MAC 地址来搜索下一个中转目标。具体过程如下图:
IP协议

链路层 传输数据的硬件部分

在网络层找到对方的 MAC 地址后,就将数据发送到数据链路层传输。至此请求报文已发出,客户端发送请求的阶段结束

服务器接收报文

接收端服务器在链路层接收到数据后,删除该层的首部信息并向网络层传递,网络层将接收的数据向传输层传递,在传输层会将传输的数据按序号从组请求报文并传送给应用层。当数据传输到应用层才能算真正接收到由客户端发送过来的 HTTP 请求

应用层 服务器发送 HTTP 响应报文

下面对百度首页响应报文首部进行分析:
状态行

协议版本 状态码 状态码原因短语
HTTP/1.1 200 OK

首部字段

当前服务器上安装的HTTP服务器程序信息
bfe:Baidu Front End。百度人自己写的反向代理及防攻击接入层
Server: bfe/1.0.8.18
响应日期时间
Date: Thu, 08 Dec 2016 14:48:19 GMT
说明报文实体的媒体类型
Content-Type: text/html; charset=utf-8
传输编码方式:分块编码
Transfer-Encoding: chunked
链接方式:持久链接  http/1.1之后这个已经没必要了
Connection: keep-alive
只接受对持相同自然语言的请求返回缓存
Vary: Accept-Encoding
缓存控制:仅向特定用户返回响应
Cache-Control: private
Cxy_all: baidu+43a6e396a3ed26dc7d1de13c6af79e49
缓存过期时间
Expires: Thu, 08 Dec 2016 14:47:38 GMT
X-Powered-By: HPHP
X-UA-Compatible: IE=Edge,chrome=1
Strict-Transport-Security: max-age=172800
BDPAGETYPE: 1
BDQID: 0xc9d964a600018bb8
BDUSERID: 0
设置cookie
Set-Cookie: H_PS_PSSID=1451_21116_17001_21408_21417_21554_20929; path=/;

响应报文的传输方式与请求报文相同,简单点说就是原路返回
在响应报文中我们通过 Chrome DevTool 的 Network 面板可以看到输入的www.baidu.com会被重定向到https://www.baidu.com/,点击重定向后的www.baidu.com,在右边的Response面板中可以看到客户端接收到的报文实体即返回的HTML页面代码

网络通信流程图
网络通信流程图

在网络通信阶段对前端优化建议:

  1. 减少 HTTP 请求数
    1. 合并资源,如合并 JavaScript 文件、CSS 文件,利用 CSS Sprite 合并图片等
    2. 内联图片,data url 节省了 HTTP 请求,但是如果这个图像在网页多个地方显示会加大网页的内容,延长下载时间。
  2. 域名提前解析,在页面中不同域名的链接需指定预取域名:<link rel="dns-prefetch" href="http://this-is-a.com">,IE9+支持
  3. 避免重定向(重定向会增加 http 请求的次数)
  4. cookie 优化,cookie 越多会导致请求头越大
  5. 启用 GZIP 压缩(Accept-Encoding:g-zip)
  6. 使用 CDN 加速,减小服务器压力
  7. 合理利用 HTTP 缓存,通过设置 Expires

页面渲染

客户端在接收到 html 代码之后,接下来的流程如下:

解析 html 以构建 DOM 树

解析一个文档即将其转换为具有一定意义的结构(编码可以理解和使用的东西)。解析的结果通常是表达文档结构的节点树,称为解析树或语法树。
解析是以文档所遵循的语法规则(编写文档所用的语言或格式)为基础的。所有可以解析的格式都必须对应确定的语法(由词汇和语法规则构成)。这称为与上下文无关的语法。
解析的过程可以分成两个子过程:词法分析和语法分析。

  • 词法分析是将输入内容分割成大量标记的过程。标记是语言中的词汇,即构成内容的单位。在人类语言中,它相当于语言字典中的单词。
  • 语法分析是应用语言的语法规则的过程。

解析器通常将解析工作分给以下两个组件来处理:

  • 词法分析器(有时也称为标记生成器),负责将输入内容分解成一个个有效标记;
  • 而解析器负责根据语言的语法规则分析文档的结构,从而构建解析树。

由于不能使用常规的解析技术,浏览器就创建了自定义的解析器来解析 HTML。此解析算法由两个阶段组成:标记化和树构建。
具体的解析过程可参考浏览器的工作原理中的标记化算法和构建树算法

解析器的输出“解析树”是由 DOM 元素和属性节点构成的树结构。DOM 是文档对象模型 (Document Object Model) 的缩写。它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的接口。解析树的根节点是“Document”对象。

当解析到 link 标签时会请求相应的 CSS 文件,并将其 CSS 规则解析为 StyleSheet 对象,CSS 文件中的其他外链资源如背景图片等只有等到其规则与 DOM 树某节点相匹配时才会加载
当解析遇到 img 标签时会根据路径向服务器相应的资源文件夹中请求图片资源,但并不会等待图片资源下载完再去解析接下来的 html,而是并发执行即图片资源仍在下载,html 解析也在进行。如果没有定义图片的 height 和 width 属性,那么浏览器为了能够显示每一个加载的图像,它需要先下载图像,然后解析出图像的高度和宽度,并在显示窗口留出相应的屏幕空间,这样就会导致浏览器不断地重新计算/调整页面的布局,这可能会延迟文档的显示,并导致页面重绘。
当解析遇到 script 标签时,将启动 JavaScript 引擎,这时将阻塞 DOM 树的构建。因为 JavaScript 执行过程中, JavaScript 很可能会对 DOM 树进行读写操作。直到 JavaScript 执行完毕(此时执行的是全局对象初始创建和全局上下文中代码的执行),DOM 树才会恢复构建。

构建 render 树

为了更好地用户体验效果,浏览器会在构建 DOM 树的同时,也在构建 render 树。呈现树的每一个节点即为与其相对应的 DOM 节点的 CSS 框,框的类型与 DOM 节点的 display 属性有关,block 元素生成 block 框,inline 元素生成 inline 框。每一个呈现树节点都有与之相对应的 DOM 节点,但 DOM 节点不一定有与之相对应的呈现树节点,比如 display 属性为 none 的 DOM 节点,而且呈现树节点在呈现树中的位置与他们在 DOM 树中的位置不一定相同,比如 float 与绝对定位元素。在构建 render 树的时候需要为 DOM 树匹配 CSS 规则,在这个阶段因为匹配规则是从右往左匹配的,所以 css 的编写规则很重要。不好的 CSS 选择器写法会影响到页面渲染的效率,具体是如何编写高效的 CSS 规则的可参考这篇文章CSS 选择器性能分析

布局 render 树

在创建 render 树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排。布局是一个递归的过程,它从根元素开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的呈现器计算几何信息。
布局通常具有以下模式:

  1. 父呈现器确定自己的宽度。
  2. 父呈现器依次处理子呈现器,并且:
    1. 放置子呈现器(设置 x,y 坐标)。
    2. 如果有必要,调用子呈现器的布局,这会计算子呈现器的高度。
  3. 父呈现器根据子呈现器的累加高度以及边距和补白的高度来设置自身高度,此值也可供父呈现器的父呈现器使用。
  4. 将其 dirty 位设置为 false

绘制 render 树

在绘制阶段,系统会遍历 render 树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。绘制工作是使用用户界面基础组件完成的。和布局一样,绘制也分为全局(绘制整个呈现树)和增量两种。在增量绘制中,部分呈现器发生了更改,但是不会影响整个树。更改后的呈现器将其在屏幕上对应的矩形区域设为无效,这导致 OS 将其视为一块“dirty 区域”,并生成“paint”事件。
绘制顺序:

  1. 背景颜色
  2. 背景图片
  3. 边框
  4. 子代
  5. 轮廓

页面变化造成的影响

在发生变化时,浏览器会尽可能做出最小的响应。因此,元素的颜色改变后,只会对该元素进行重绘。元素的位置改变后,只会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大“html”元素的字体)会导致缓存无效,使得整个呈现树都会进行重新布局和绘制。

在页面渲染阶段对前端优化建议:

  • 建议将 CSS 文件放在页首,以便构建 DOM 树;而将 JavaScript 文件尽量放在页面下方,防止阻塞构建 DOM 树;而 JavaScript 的 onload 事件里,不要写太多影响首屏渲染的、操作 DOM 树的 JavaScript 代码。
  • 精简 JavaScript 和 CSS 代码,并进行代码压缩,使得 css 和 js 资源更快的下载
  • 编写高效的 CSS 代码
  • 重要的图片或者想让用户优先看到的图片使用 img 标签,次要的图片使用 background 引入

参考文献:

  1. 《图解 HTTP》
  2. 浏览器的工作原理
  3. 《高性能网站建设指南》

由于个人水平有限,不够详细或有误的地方还望指出,共同进步才是最好的结果