likes
comments
collection
share

输入⼀个URL发生了什么

作者站长头像
站长
· 阅读数 12

用户输入

当用户开始在导航栏上面输入内容的时候。UI线程要进行一系列的解析来判定是将用户输入发送给搜索引擎还是直接请求你输入的站点资源。

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

当用户按下回车键的时候,UI线程会通知网络进程初始化一个网络请求来获取站点的内容。

这时候tab上的icon会展示一个提示资源正在加载中的旋转圈圈,而且网络进程会进行一系列诸如DNS寻址以及为请求建立TLS连接的操作。

  • tips: 这时如果网络进程收到服务器的HTTP 301重定向响应,它就会告知UI线程进行重定向然后它会再次发起一个新的网络请求。

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

当浏览器刚开始加载一个地址之后,标签页上的图标便进入了加载状态(loading)。但此时图中页面显示的依然是之前打开的页面内容,并没立即替换为需要跳转的页面。因为需要等待提交文档阶段,页面内容才会被替换。

URL请求过程

接下来,便进入了页面资源请求过程。这时,浏览器主进程收到了地址栏的用户输入,

因为浏览器是分为

  • Browser(一个) - 浏览器进程, 只有一个浏览器进程,负责浏览器的主体部分,包括导航栏,书签, 前进和后退按钮, 提供存储等功能
  • Network(一个) - 网络进程, 主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程
  • GPU(一个) - 图像渲染进程, 其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。负责独立于其它进程的GPU任务。它之所以被独立为一个进程是因为它要处理来自于不同tab的渲染请求并把它在同一个界面上画出来。
  • Extensions(多个) - 扩展程序进程
  • 其他进程 - 工具进程,辅助框架等等

浏览器发起请求是需要调用网络进程的,因为进程之间的内容是需要相互隔离的,进程隔离是为保护操作系统中进程互不干扰的技术,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,当需要跨进程通信的时候,浏览器进程会通过进程间通信机制(IPC)把 URL 请求发送至网络进程。

网络进程接收到 URL 请求后,会在这里发起真正的 URL 请求流程。

首先,网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行 DNS 解析,以获取请求域名的服务器 IP 地址。

DNS解析

因为ip地址是一种用于互联网上访问内容的虚拟地址,要想访问他人的网络就要记住ip地址才行,因为互联网不能通过域名直接访问,系统就会去hosts文件里,去查询这个域名对应的ip地址是什么。然后电脑会去访问这个ip。因为电脑不可能保存所有的域名和对应的ip地址。所以单独出来了一个服务器,让服务器去专门存储这个世界上绝大多数域名和ip的映射表,这个服务器就是dns服务器。

如果请求协议是 HTTPS,那么还需要建立 TLS 连接。

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

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

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

在导航过程中,如果服务器响应行的状态码包含了 301、302 一类的跳转信息,浏览器会跳转到新的地址继续导航;如果响应行是 200,那么表示浏览器可以继续处理该请求。

响应数据处理

在处理了跳转信息之后,浏览器会根据 Content-Type 的值来决定如何显示响应体的内容。

不同 Content-Type 的后续处理流程也截然不同。如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是 HTML,那么浏览器则会继续进行导航流程。由于 Chrome 的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。

网络进程在把内容交给渲染进程之前还会对内容做SafeBrowsing检查,如果请求的域名或者响应的内容和某个已知的病毒网站相匹配,网络进程会给用户展示一个警告的页面。除此之外,网络进程还会做CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。

寻找一个渲染进程来绘制页面

在网络进程做完所有的检查后并且能够确定浏览器应该导航到该请求的站点,它就会告诉UI线程所有的数据都已经被准备好了。

UI线程在收到网络进程的确认后会为这个网站寻找一个渲染进程(renderer process)来渲染界面

因为网络请求的耗时可能会很长, 所以第二步中当UI线程发送URL链接给网络进程后,它其实已经知晓它们要被导航到哪个站点了。

所以在网络进程干活的时候,UI线程会主动地为这个网络请求启动一个渲染线程。如果一切顺利的话(没有重定向之类的东西出现),网络进程准备好数据后页面的渲染进程已经就准备好了,这就节省了新建渲染进程的时间。

不过如果发生诸如网站被重定向到不同站点的情况,刚刚那个渲染进程就不能被使用了,它会被摒弃,一个新的渲染进程会被启动。

提交导航

到这一步的时候,数据和渲染进程都已经准备好了,浏览器进程(browser process)会通过IPC告诉渲染进程去提交本次导航(commit navigation)。

除此之外浏览器进程还会将刚刚接收到的响应数据流传递给对应的渲染进程让它继续接收到来的HTML数据。

一旦浏览器进程收到渲染线程的回复说导航已经被提交了(commit),导航这个过程就结束了,文档的加载阶段(document loading phase)会正式开始。

到了这个时候,导航栏会被更新,安全指示符和站点设置会展示新页面相关的站点信息。 当前tab的会话历史(session history)也会被更新,这样当你点击浏览器的前进和后退按钮也可以导航到刚刚导航完的页面。为了方便你在关闭了tab或窗口(window)的时候还可以恢复当前tab和会话(session)内容,当前的会话历史会被保存在磁盘上面。

加载完成

当导航提交完成后,渲染进程开始着手加载资源以及渲染页面。

一旦渲染进程完成渲染(load),它会通过IPC告知浏览器进程,然后UI线程就会停止导航栏上旋转的圈圈。

准备渲染进程

到这一步的时候,数据和渲染进程都已经准备好了,浏览器进程(browser process)会通过IPC告诉渲染进程去提交本次导航(commit navigation)。

除此之外浏览器进程还会将刚刚接收到的响应数据流传递给对应的渲染进程让它继续接收到来的HTML数据。

一旦浏览器进程收到渲染线程的回复说导航已经被提交了(commit),导航这个过程就结束了,文档的加载阶段(document loading phase)会正式开始。

到了这个时候,导航栏会被更新,安全指示符和站点设置会展示新页面相关的站点信息。当前tab的会话历史(session history)也会被更新,这样当你点击浏览器的前进和后退按钮也可以导航到刚刚导航完的页面。为了方便你在关闭了tab或窗口(window)的时候还可以恢复当前tab和会话(session)内容,当前的会话历史会被保存在磁盘上面。

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

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

要解决这个问题,我们就需要先了解下什么是同一站点(same-site)。具体地讲,我们将“同一站点”定义为根域名(例如,geekbang.org)加上协议(例如,https:// 或者 http://),还包含了该根域名下的所有子域名和不同的端口,比如下面这三个:

https://time.geekbang.org
https://www.geekbang.org
https://www.geekbang.org:8080

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

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

  • 通常情况下,打开新的页面都会使用单独的渲染进程;
  • 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。

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

提交文档

所谓提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:

  • 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
  • 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
  • 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
  • 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。

其中,当渲染进程确认提交之后,更新内容如下图所示:

输入⼀个URL发生了什么 这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,之前的页面没有立马消失,而是要加载一会儿才会更新页面。

渲染阶段

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

渲染进程的主要任务是将HTML,CSS,以及JavaScript转变为我们可以进程交互的网页内容。

渲染进程里面有:一个主线程(main thread),几个工作线程(worker threads),一个合成线程(compositor thread)以及一个光栅线程(raster thread)

在渲染进程里面,主线程(main thread)处理了绝大多数你发送给用户的代码。如果你使用了web worker或者service worker,相关的代码将会由工作线程(worker thread)处理。合成(compositor)以及光栅(raster)线程运行在渲染进程里面用来高效流畅地渲染出页面内容。

构建Dom

渲染进程在导航结束的时候会收到来自浏览器进程提交导航的消息,在这之后渲染进程就会开始接收HTML数据,同时主线程也会开始解析接收到的文本数据,并把它转化为一个DOM(Document Object Model)对象。

上面提到过,渲染进程在导航结束的时候会收到来自浏览器进程提交导航的消息,在这之后渲染进程就会开始接收HTML数据,同时主线程也会开始解析接收到的文本数据,并把它转化为一个DOM(Document Object Model)对象。

DOM对象既是浏览器对当前页面的内部表示,也是Web开发人员通过JavaScript与网页进行交互的数据结构以及API。

如何将HTML文档解析为DOM对象是在HTML标准中定义的。

不过在你的web开发生涯中,你可能从来没有遇到过浏览器在解析HTML的时候发生错误的情景。

这是因为浏览器对HTML的错误容忍度很大。举些例子:如果一个段落缺失了闭合p标签(

),这个页面还是会被当做为有效的HTML来处理;

Hi! <b>I'm <i>Chrome</b>!</i> 

虽然有语法错误,不过浏览器会把它处理为

Hi! <b>I'm <i>Chrome</i></b><i>!</i>。
子资源加载

除了HTML文件,网站通常还会使用到一些诸如图片,CSS样式以及JavaScript脚本等子资源,这些文件会从缓存或者网络上获取。

主线程会按照在构建DOM树时遇到各个资源的循序一个接着一个地发起网络请求,为了提升效率,浏览器会同时运行“预加载扫描”程序。

如果在HTML文档里面存在诸如

<img src="转存失败,建议直接上传图片文件 " alt="转存失败,建议直接上传图片文件">

或者<link>这样的标签,

预加载扫描程序会在HTML解析器里面找到对应要获取的资源,并把这些要获取的资源告诉浏览器进程里面的网络线程

JavaScript会阻塞HTML的解析过程

当HTML解析器碰到script标签的时候,它会停止HTML文档的解析从而转向JavaScript代码的加载,解析以及执行。

为什么要这样做呢?因为script标签中的JavaScript可能会使用诸如document.write()这样的代码改变文档流(document)的形状,从而使整个DOM树的结构发生根本性的改变。因为这个原因,HTML解析器不得不等JavaScript执行完成之后才能继续对HTML文档流的解析工作。

开发者可以通过很多方式告诉浏览器如何才能更加优雅地加载网页需要用到的资源。比如你可以为script标签添加一个async或者defer属性来使JavaScript脚本进行异步加载。资源预加载可以用来告诉浏览器这个资源在当前的导航肯定会被用到,你想要尽快加载这个资源。

样式计算 CSS

主线程会解析页面的CSS从而确定每个DOM节点的计算样式(computed style)。计算样式是主线程根据CSS样式选择器(CSS selectors)计算出的每个DOM元素应该具备的具体样式,即使你的页面没有设置任何自定义的样式,每个DOM节点还是会有一个计算样式属性,这是因为每个浏览器都有自己的默认样式表。 因为这个样式表的存在,页面上的h1标签一定会比h2标签大,而且不同的标签会有不同的magin和padding

布局 Layout

前面这些步骤完成之后,渲染进程就已经知道页面的具体文档结构以及每个节点拥有的样式信息了,可是这些信息还是不能最终确定页面的样子.

只知道网站的文档流以及每个节点的样式是远远不足以渲染出页面内容的,还需要通过布局(layout)来计算出每个节点的几何信息。

布局的具体过程是:

  1. 主线程会遍历刚刚构建的DOM树,根据DOM节点的计算样式计算出一个布局树(layout tree)。
  2. 布局树上每个节点会有它在页面上的x,y坐标以及盒子大小(bounding box sizes)的具体信息。布局树长得和先前构建的DOM树差不多,不同的是这颗树只有那些可见的(visible)节点信息。

举个例子,如果一个节点被设置为了display:none,这个节点就是不可见的就不会出现在布局树上面(visibility:hidden的节点会出现在布局树上面)。同样的,如果一个伪元素(pseudo class)节点有诸如p::before{content:"Hi!"}这样的内容,它会出现在布局上,而不存在于DOM树上。

绘画

知道了DOM节点以及它的样式和布局其实还是不足以渲染出页面来的。

为什么呢?举个例子,假如你现在想对着一幅画画一幅一样的画,你已经知道了画布上每个元素的大小,形状以及位置,你还是得思考一下每个元素的绘画顺序,因为画布上的元素是会互相遮挡的(z-index)。

如果页面上的某些元素设置了z-index属性,绘制元素的顺序就会影响到页面的正确性。

合成

浏览器已经知道了关于页面以下的信息:文档结构,元素的样式,元素的几何信息以及它们的绘画顺序。那么浏览器是如何利用这些信息来绘制出页面来的呢?将以上这些信息转化为显示器的像素的过程叫做光栅化(rasterizing)。

现代浏览器采用合成的方式, 来展示整个页面

合成是一种将页面分成若干层,然后分别对它们进行光栅化,最后在一个单独的线程 - 合成线程(compositor thread)里面合并成一个页面的技术。当用户滚动页面时,由于页面各个层都已经被光栅化了,浏览器需要做的只是合成一个新的帧来展示滚动后的效果罢了。页面的动画效果实现也是类似,将页面上的层进行移动并构建出一个新的帧即可。

参考

浏览器工作原理与实践