01.网络与资源请求
网络与资源请求
网页加载过程
之前我们我们提到,Tab 以外的大部分工作由浏览器进程 Browser Process 负责,针对工作的不同,Browser Process 划分出不同的工作线程:
- UI thread:控制浏览器上的按钮及输入框;
- Network thread:处理网络请求,从网上获取数据;
- Storage thread:控制文件等的访问;
处理输入
当用户开始在导航栏上面输入内容的时候,UI 线程(UI thread)做的第一件事就是询问:“你输入的字符串是一些搜索的关键词(search query)还是一个 URL 地址呢?”。因为对于 Chrome 浏览器来说,导航栏的输入既可能是一个可以直接请求的域名还可能是用户想在搜索引擎(例如 Google)里面搜索的关键词信息,所以当用户在导航栏输入信息的时候 UI 线程要进行一系列的解析来判定是将用户输入发送给搜索引擎还是直接请求你输入的站点资源。
开始导航
回车按下后,UI thread 将关键词搜索对应的 URL 或输入的 URL 交给网络线程 Network thread,此时 UI 线程使 Tab 前的图标展示为加载中状态,然后网络进程进行一系列诸如 DNS 寻址,建立 TLS 连接等操作进行资源请求,如果收到服务器的 301 重定向响应,它就会告知 UI 线程进行重定向然后它会再次发起一个新的网络请求。
URL 解析浏览器会首先判断你输入的是否为有效的 URL,还是属于需要传输给搜索引擎的默认搜索关键字。并且浏览器还会检查自带的 “ 预加载 HSTS(HTTP 严格传输安全)” 列表,这个列表里包含了那些请求浏览器只使用 HTTPS 进行连接的网站。如果网站在这个列表里,浏览器会使用 HTTPS 而不是 HTTP 协议,否则,最初的请求会使用 HTTP 协议发送。接下来,浏览器会检查你的输入字符是否含有特殊非 ASCII 关键字,如果含有特殊的字符会进行 UTF8 编码,部分特殊的网站会要求进行 GBK 编码。
DNS 解析在 URL 解析完成之后,浏览器会根据本地的 hosts 文件或者向本机 / 网关设置的 DNS 服务器发起域名解析请求。DNS 的解析过程中,浏览器会首先检查本机是否有域名缓存,如果没有的话会向 DNS 服务器发起请求,如果子 DNS 服务器不存在该记录则会递归向父层级的 DNS 发起请求。我之前在进行 iOS 开发的时候还碰到 IPV6 的问题,即苹果要求 iOS 应用能够在 IPV6 环境下正常运行,那么这个时候 DNS 服务器发现如果你请求的是 IPV6 的地址,对于仅有 IPV4 地址的服务器其会提供一个 NAT64 的功能,即保证客户端虽然为 IPV6 地址,也能和 IPV4 的服务器正常通信。
读取响应
Network thread 接收到服务器的响应后,开始解析 HTTP 响应报文,然后根据响应头中的 Content-Type 字段来确定响应主体的媒体类型(MIME Type)。
如果媒体类型是一个 HTML 文件,则将响应数据交给渲染进程(renderer process)来进行下一步的工作,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。
与此同时,浏览器会进行 Safe Browsing 安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。除此之外,网络线程还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。
寻找一个渲染进程(renderer process)
各种检查完毕以后,Network thread 确信浏览器可以导航到请求网页,Network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。
浏览器为了对查找渲染进程这一步骤进行优化,考虑到网络请求获取响应需要时间,所以在第二步开始,浏览器已经预先查找和启动了一个渲染进程,如果中间步骤一切顺利,当 network thread 接收到数据时,渲染进程已经准备好了,但是如果遇到重定向,这个准备好的渲染进程也许就不可用了,这个时候会重新启动一个渲染进程。
提交(commit)导航
到了这一步,数据和渲染进程都准备好了,Browser Process 会向 Renderer Process 发送 IPC 消息来确认导航,此时,浏览器进程将准备好的数据发送给渲染进程,渲染进程接收到数据之后,又发送 IPC 消息给浏览器进程,告诉浏览器进程导航已经提交了,页面开始加载。
这个时候导航栏会更新,安全指示符更新(地址前面的小锁),访问历史列表(history tab)更新,即可以通过前进后退来切换该页面。当导航提交完成后,渲染进程开始加载资源及渲染页面(详细内容下文介绍),当页面渲染完成后(页面及内部的 iframe 都触发了 onload 事件),会向浏览器进程发送 IPC 消息,告知浏览器进程,这个时候 UI thread 会停止展示 tab 中的加载中图标。
当导航提交完成后,渲染进程开始着手加载资源以及渲染页面。我会在后面的文章中讲述渲染进程渲染页面的具体细节。一旦渲染进程“完成”(finished)渲染,它会通过 IPC 告知浏览器进程(注意这发生在页面上所有帧(frames)的 onload 事件都已经被触发了而且对应的处理函数已经执行完成了的时候),然后 UI 线程就会停止导航栏上旋转的圈圈。
切换站点
一个最简单的导航情景已经描述完了!可是如果这时用户在导航栏上输入一个不一样的 URL 会发生什么呢?如果是这样,浏览器进程会重新执行一遍之前的那几个步骤来完成新站点的导航。不过在浏览器进程做这些事情之前,它需要让当前的渲染页面做一些收尾工作,具体就是询问一下当前的渲染进程需不需要处理一下 beforeunload 事件。
beforeunload 可以在用户重新导航或者关闭当前 tab 时给用户展示一个“你确定要离开当前页面吗?”的二次确认弹框。浏览器进程之所以要在重新导航的时候和当前渲染进程确认的原因是,当前页面发生的一切(包括页面的 JavaScript 执行)是不受它控制而是受渲染进程控制,所以它也不知道里面的具体情况。
不过,不要随便给页面添加 beforeunload 事件监听,你定义的监听函数会在页面被重新导航的时候执行,因此这会增加重导航的时延。beforeunload 事件监听函数只有在十分必要的时候才能被添加,例如用户在页面上输入了数据,并且这些数据会随着页面消失而消失。
如果重新导航是在页面内被发起的呢?例如用户点击了页面的一个链接或者客户端的 JavaScript 代码执行了诸如 window.location = "newsite.com"
的代码。这种情况下,渲染进程会自己先检查一个它有没有注册 beforeunload 事件的监听函数,如果有的话就执行,执行完后发生的事情就和之前的情况没什么区别了,唯一的不同就是这次的导航请求是由渲染进程给浏览器进程发起的。如果是重新导航到不同站点(different site)的话,会有另外一个渲染进程被启动来完成这次重导航,而当前的渲染进程会继续处理现在页面的一些收尾工作,例如 unload 事件的监听函数执行。
Service Worker 与预加载
因为 Service worker 可以用来写网站的网络代理(network proxy),所以开发者可以对网络请求有更多的控制权,例如决定哪些数据缓存在本地以及哪些数据需要从网络上面重新获取等等。如果开发者在 service worker 里设置了当前的页面内容从缓存里面获取,当前页面的渲染就不需要重新发送网络请求了,这就大大加快了整个导航的过程。
service worker 在注册的时候,它的作用范围(scope)会被记录下来。在导航开始的时候,网络线程会根据请求的域名在已经注册的 service worker 作用范围里面寻找有没有对应的 service worker。如果有命中该 URL 的 service worker,UI 线程就会为这个 service worker 启动一个渲染进程(renderer process)来执行它的代码。Service worker 既可能使用之前缓存的数据也可能发起新的网络请求。
UI 线程会启动一个渲染进程来运行找到的 Service Worker 代码,代码具体是由渲染进程里面的工作线程(worker thread)执行。
在上面的例子中,你应该可以感受到如果启动的 service worker 最后还是决定发送网络请求的话,浏览器进程和渲染进程这一来一回的通信包括 service worker 启动的时间其实增加了页面导航的时延。导航预加载就是一种通过在 service worker 启动的时候并行加载对应资源的方式来加快整个导航过程效率的技术。预加载资源的请求头会有一些特殊的标志来让服务器决定是发送全新的内容给客户端还是只发送更新了的数据给客户端。