浏览器导航流程

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

这个是一道很经典的面试题,它能比较全面的考察一个人的网络、操作系统、Web 等相关知识。同时还可以把网络、操作系统、HTML、CSS、JS 这些知识串联起来。那么今天我们就来探索一下这个流程。

上图就是从输入 URL 到页面展示完整流程示意图。我们先从大体上过一遍这个流程,然后在去看具体的细节。

  1. 首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。
  2. 然后,在网络进程中发起真正的 URL 请求。
  3. 接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
  4. 浏览器进程接收到网络进程的响应头数据之后,发送“提交导航 (CommitNavigation)”消息到渲染进程。
  5. 渲染进程接收到“提交导航”的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道。
  6. 最后渲染进程会向浏览器进程“确认提交”,这是告诉浏览器进程:“已经准备好接受和解析页面数据了”。
  7. 浏览器进程接收到渲染进程“提交文档”的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。

在浏览器中,从用户发出 URL 请求到页面开始解析的这个过程,就叫做导航。下面我们就来详细分析一下各个阶段的细节。

从输入 URL 到页面展示

  1. 用户输入

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

    • 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL。

    • 如果判断输入内容符合 URL 规则,那么地址栏会根据规则,把这段内容加上协议,合成为完整的 URL。

      当用户输入关键字并键入回车之后,这意味着当前页面即将要被替换成新的页面,不过在这个流程继续之前,浏览器还给了当前页面一次执行 beforeunload 事件的机会,beforeunload 事件允许页面在退出之前执行一些数据清理操作,还可以询问用户是否要离开当前页面,比如当前页面可能有未提交完成的表单等情况,因此用户可以通过 beforeunload 事件来取消导航,让浏览器不再执行任何后续工作。

      当我们按下回车后,标签页上的图标就变成了一个 loading 状态,但是这个时候的页面还是之前的页面。当渲染进程发出“提交文档”的信息后,页面的内容才会被替换。

  2. URL 请求过程

    接下来,便进入了页面资源请求过程。这时,浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会在这里发起真正的 URL 请求流程。那具体流程是怎样的呢?

    首先,网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。

    请求的第一步是要进行 DNS 解析。这个时候 Chrome 会先去查它自己的 DNS 缓存,再查系统缓存,再查 host 文件,然后再逐级往上查,直到根域名服务器。为了提高查询效率,这里的每一层都会有很多缓存。

    解析完 DNS 后,便可以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。

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

    服务器收到请求信息后,会根据请求信息生成响应数据,然后发送给浏览器的网络进程。等网络进程收到响应头后,就开始解析响应头的内容。

    如果响应头中的状态码是301或302,那么就说明服务器需要浏览器重定向其他 URL,这个时候网络进程会从响应头的 Location 字段里面取出重定向的地址,然后发起新的请求,这个流程就会重头开始。

    如果响应头中的状态码是200,那么网络进程就可以继续处理这个请求。在响应头中有一个非常重要的字段,叫 Content-Type ,它的作用是告诉浏览器服务器返回的响应体数据是什么类型的。然后浏览器会根据 Content-Type 字段的值来决定如何处理响应体的内容。

    比如,content-type 的值是 text/html,浏览器就知道服务器返回的数据是 HTML 格式。或者 content-type 的值是 application/octet-stream,这个格式就是一个字节流类型,通常情况,浏览器会判断它为下载类型,那么这个内容会被提交给浏览器的下载管理器,然后去下载它,同时这个 URL 请求的导航流程就到此结束了。但如果是 html 类型,那么浏览器会继续下面的导航流程。

    关于浏览器是如何解析服务端发来的字节流(Bytes),下面有个 Toy 版的实现。实现原理就是使用状态机逐个解析响应头中的 statusLine、headers、body,然后封装成对象。

    huyinglin/ToyBrowser

  3. 准备渲染进程

    默认情况下,Chrome会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,也有一些列外,在某些情况下,浏览器会让多个页面直接运行在同一个渲染进程中。

    渲染进程

    比如我从百度里打开了另外一个页面—百度搜索,我们看下面的任务管理器截图:

从图中可以看出,打开的这三个页面都是运行在同一个渲染进程中的,进程ID是76086.

那什么情况下会多个页面同时运行在一个渲染进程中呢?

首先我们要先了解一下什么是同一站点(same-site)。具体来讲,同一站点定义如下:

  • 跟域名相同
  • 协议相同
  • 包含该域名下所有的子域名和不同的端口

这里需要注意,同一站点和同源策略是有区别的,同源是指:域名、协议、端口相同。

Chrome 的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫 process-per-site-instance。

总结来说,打开一个新页面采用的渲染进程策略就是:

  • 通常情况下,打开新的页面都会使用单独的渲染进程。
  • 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。
  • 如果连接里面使用了 rel=”noopener noreferrer”这个属性,就会新打开一个渲染进程。使用 noopener noreferrer 就是告诉浏览器,新打开的子窗口不需要访问父窗口的任何内容,这是为了防止一些钓鱼网站窃取父窗口的信息。

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

4. 提交文档

所谓提交文档,就是渲染进程要告诉浏览器进程,我已经收到网络进程的数据了,已经开始解析了,你可以更新页面状态了。

此时响应报文在网络进程那里,需要浏览器进程做一个统一调度,跟渲染进程说,你可以去接收响应报文了。然后渲染进程就通过 IPC 与网络进程通信,让网络进程把响应报文全部发过来。

渲染进程拿到所有响应报文,就会回复浏览器进程全部响应报文都拿到了,你浏览器进程可以更新界面,我渲染进程可以进行渲染。浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态。包括安全状态、地址栏的 URL、前进后退的历史状态,同时会停止标签页上的 loading 动画,然后更新 Web 页面。但这个时候的页面是白屏的。

总结

到这里,一个完整的导航流程就“走”完了,这之后就要进入渲染阶段了,关于渲染流程我会在下篇文章仔细介绍。 一旦文档被提交,渲染进程便开始页面解析和子资源加载了。渲染完成后,一个完整的页面就生成了。