二 从输入URL到页面展示,中间发生了什么?

314 阅读12分钟

一 用户输入

浏览器进程接收用户输入的 URL 请求,并将 URL 转发给网络进程。

1.1 输入

在浏览器地址栏中输入关键字,地址栏判断该关键字是搜索内容,还是请求的 URL。

  • 如果是搜索内容:地址栏使用浏览器默认的搜索引擎,合成新的带搜索关键字的 URL;
  • 如果是 URL:地址栏根据相应规则,把 URL 加上协议,合成为完整的 URL。

1.2 回车

按下回车键,在执行搜索之前,当前页面可以监听执行beforeunload事件,该事件可以询问用户是否真的要离开当前页面,从而阻止后续的搜索跳转的工作;也可以执行清理数据等相关操作。

如果没有监听 beforeunload事件,或者放行了该事件,那么开始加载这个地址,标签页上出现加载的状态(如下图)。

二 URL 请求

浏览器进程通过 IPC(进程间通信机制)把 URL 请求发送到网络进程,网络进程接收到 URL 后,发起真正的 URL 请求。

URL 请求涉及两个重要的协议:HTTP 协议和 TCP 协议。

两者的关系:HTTP 协议属于应用层,TCP 协议属于传输层,HTTP 协议位于 TCP 协议上层。请求方要发送的数据包,在应用层加上 HTTP 头以后交给传输层的 TCP 协议处理;响应方将接收到的数据包,在传输层拆掉 TCP 头后交给应用层的 HTTP 协议处理。建立TCP 连接后顺序收发数据,双方都必须依据 HTTP 规范构建和解析 HTTP 报文。

2.1 构建请求

浏览器构建请求行信息,准备发起网络请求。

// 请求行信息示例。各部分含义:请求方法 请求URI HTTP协议版本
GET /index.html HTTP1.1

2.2 查找缓存

发起网络请求之前,先去浏览器缓存中查找缓存(分为强缓存和协商缓存)。

如果是“强缓存”(请求头的Cache-Control 或 Expires 字段),则会拦截请求,不再去与服务器建立连接,直接获取本地的资源副本。

如果“强缓存”查找失败,则进入网络请求。

2.3 TCP 连接

浏览器使用 HTTP 协议作为应用层协议,用来封装请求的文本信息;并使用 TCP/IP 作传输层协议建立网络通信传输管道,所以在 HTTP 工作开始之前,浏览器需要通过 TCP 与服务器建立连接。

2.3.1 IP 和 端口号

DNS(域名系统),发起网络请求把地址栏输入的域名(URL)解析为对应的 IP,并保存到缓存里,以供后续直接使用。URL 上一般都带有端口号,如果没有,HTTP 协议默认是80。

IP:相当于通信地址,代表着信息要发到具体的哪个主机上

端口:代表信息要发到主机上具体的哪个应用程序

2.3.2 建立 TCP 连接

TCP 的作用是保证数据能完整的传输。

获取到 IP 和端口号之后,进入 TCP 连接队列。

Chrome 存在“同一域名同时最多只能建立6个 TCP 连接”的机制,多余的请求会进入队列中等待。等待结束之后,通过“三次握手”和服务器正式建立连接。

2.4 HTTP 请求与响应

TCP 连接建立之后,浏览器和服务器便能通信了。HTTP 的数据通过TCP建立的通信管道进行数据的传输。

2.4.1 重定向

在接收到服务器的响应头后,网络进程开始解析响应头信息,如果返回的状态码是 301(永久重定向)或者是 302(临时重定向),那么浏览器会重定向到其他 URL,网络进程会从响应头中的 Location 字段读取重定向的地址,重新发起请求。

2.4.2 响应数据类型

响应头中的 Content-Type字段的值表示服务器返回的响应体数据是什么类型的数据。

如果是“text/html”,则表示是 HTML 格式的数据,接下来便开始准备渲染进程;

如果是"application/octet-stream",则是字节流类型的数据,浏览器会按照下载类型来处理,交由浏览器的下载管理器处理。

2.5 断开 TCP 连接

一般情况下,一旦服务器向客户端响应了相关数据,TCP 连接就会被关闭。

如果要持续保持该 TCP 连接,需要在请求或响应的头信息中加入Connection: Keep-Alive。这样下次请求时,不用再重复建立 TCP 的过程,提升资源的加载速度。

三 准备渲染进程

在这个阶段,浏览器进程判断是否需要新开一个渲染进程,还是复用原来的渲染进程,并准备好这个渲染进程,以接收随后到来的 HTML 数据。

在上一章提到,默认的情况下,每个标签页使用独立的渲染进程;如果从一个页面打开新的页面,而新页面与当前页属于同一站点时,那么新页面和父页面会使用同一渲染进程;嵌套的 iframe 使用独立的渲染进程。

此时,数据还在网络进程中,尚没有提交给渲染进程。

四 提交文档

这个阶段,浏览器进程把从网络进程接收到的 HTML 数据提交给渲染进程。流程如下:

  1. 浏览器进程接收到网络进程的响应头数据之后,向渲染进程发起“提交文档”的消息。
  2. 渲染进程接收到“提交文档”的消息后,和网络进程建立传输数据的“管道”。
  3. 数据传输完成后,渲染进程返回“确认提交”的消息给浏览器进程。
  4. 浏览器接收到消息后,更新浏览器的界面状态,包括安全状态、地址栏的URL、前进后退的历史状态,并开始更新页面。

五 渲染流程

文档一经提交,渲染进程对文档进行页面解析和子资源加载。依次进行如下子阶段。

5.1 构建DOM树(DOM)

浏览器无法直接理解 HTML,需要将 HTML 解析为 DOM 树。DOM 树保存在内存中,通过 Javascript 可以查询和修改(window.document)。

5.2 样式计算(Recalculate Style)

该过程,是要计算出 DOM 树每个节点中各个元素的具体样式,并将样式应用到 DOM 树上。过程如下:

  1. 把 CSS 转成浏览器可以理解的结构 styleSheets。styleSheets 具备查询和修改的功能(document.styleSheets);
  2. 将一些不被渲染引擎理解的值转换为可以理解的值。如 :
{
  font-size: 2em;
	color:blue;
  font-weight: bold;
}
/* 转换为 */
{
  font-size: 32px;
	color: rgba(0,0,255);
  font-weight: 700;
}

3. 通过 CSS 的继承原则和层叠原则,计算出 DOM 树中每个节点的具体样式。

最后得到的 DOM 节点样式如下,并将 DOM 节点样式的数据保存在 ComputedStyle 中:

5.3 布局(Layout)

有了 DOM 树 和 DOM 树中元素的样式后,接下来要计算 DOM 树中可见元素的几何位置信息。

5.3.1 创建布局树

遍历 DOM 树,把可见的元素节点添加到布局树中;忽略掉不可见的元素节点,如head标签下的所有内容,如 display: none的元素等等。

5.3.2 布局计算

布局计算是个动态的过程,在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。

5.4 分层(Layer)

渲染引擎把页面的特定节点生成不同的图层,并生成图层树(LayerTree),这些图层按照一定的顺序叠加在一起,形成最终的页面。可通过开发者工具里的“Layers”标签观察分层的情况。

并不是布局树的每个节点都生成一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层,每一个节点都会直接或者间接地从属于一个层。

渲染引擎会为满足以下条件的元素创建单独的图层:

  1. 拥有层叠上下文属性的元素: position(明确定位)/ z-index/ filter / opacity
  2. 需要裁剪(clip)的地方:比如 overflow: hidden

5.5 绘制(Paint)

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制。

渲染引擎把图层的绘制分拆成很多小的绘制指令,再把这些指令按照顺序组成一个待绘制的列表。绘制阶段要做的事实际就是输出该列表。

该列表准备好之后,渲染进程的主线程将该列表提交给合成线程(子线程),实际的绘制动作发生在合成线程。

5.6 分块(tiles)

这里由渲染进程的主线程进入子线程合成线程

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把可以看到的这个部分叫做视口(viewport)。

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

因此,合成线程会将图层划分为图块(tile)。

5.7 栅格化(raster)

栅格化,是指合成线程将视口附近的图块转换为位图。图块是栅格化的最小单位。

栅格化的过程一般交由 GPU 进程处理(跨进程 IPC),生成的位图也保存在 GPU 内存中。

5.8 合成与显示

一旦所有图块都被栅格化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。停止标签图标上的加载动画。

到这里,整个流程就结束了。

六 页面元素变动的相关策略

6.1 重排/回流(Reflow)

布局树中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建。重排需要更新完整的渲染流水线,所以开销也是最大的,如图,每个步骤都会再执行一遍。

防止布局抖动(高频率触发重排,如获取offsetTop等信息)工具: FastDom(读写分离)。

6.2 重绘(Repaint)

当布局树中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color。则就叫称为重绘。

重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

6.3 直接合成

渲染引擎将跳过布局和绘制,只执行后续的合成操作。如 css 的 transform, opacticy 属性,直接在非主线程上执行合成。相对于重绘和重排,合成能大大提升绘制效率。

七 问题

问题1:为什么很多站点第二次打开速度会很快?

DNS 缓存,浏览器把域名对应的IP关联并保存在本地,下次访问不用再次请求解析 IP。

HTTP 缓存:强缓存和协商缓存。

问题2:登录一个网站后,下次再访问时是如何保持登录状态的?

服务器接收到用户登录信息后验证,生成一段表示用户身份的字符串,把该字符串写到响应头的 Set-Cookie 字段里,发送给浏览器。

浏览器在接收到服务器的响应头后,解析响应头里的 Set-Cookie 字段,将其保存到本地。当用户再次访问时,浏览器会读取保存的 Cookie ,并把数据写进请求头里的 Cookie 字段里,发送给服务器。

服务器在收到 HTTP 请求头数据之后,查找“Cookie”字段信息,判断该用户是已登录状态,然后生成含有该用户信息的页面数据,发送给浏览器。

问题3: CSS 是否会阻塞 DOM 树的合成?是否会阻塞页面的显示?

当服务器接收 HTML 第一批数据时,DOM 解析器就开始工作了,解析的过程中,如果遇到 JS 脚本,会先下载(外联的情况)和执行 JS脚本,执行完成之后,才会继续向下解析;下载和执行的过程中,会暂停 DOM 的解析,所以 JS 会阻塞 DOM 的渲染。

这种情况下,如果 JS 脚本访问了某个元素的样式,那就需要等待这个样式被下载完成,才能继续向下执行,所以 CSS 也会阻塞 DOM 树的合成。

样式计算(Recalculate Style) ****阶段需要等待 CSS 文件的资源进行层叠样式,所以也会阻塞页面的显示。


[参考]:李冰《浏览器工作原理与实践》等网络资源