One page summary
作为面试常常被问到的一个问题 ------ 从浏览器输入 URL 到页面渲染究竟发生了什么, 这个问题囊括的知识点, 应该说是知识体系是巨大的。其中包含但不限于浏览器的多进程架构, Chromium 网络栈, Blink 渲染机制等等等等。本文通过一次较为完整的 Navigation 带大家领略一下其中奥妙。此篇文章由于篇幅限制, 将首先带大家了解一下发生在 Renderer 进程之外的事情, 后续会再有一篇《Life Of A Pixel》来介绍 Renderer 里面具体发生了什么 (Renderer 里面发生的事网络上的八股已经有所覆盖, 但是本篇文章包括这个系列的上一篇文章 《多进程架构》 却是市面上鲜有介绍的。了解这部分的原理对你理解浏览器大有裨益, 并且也能为面试加分)。
A simple navigation

第一步: 处理输入
当我们在 Chrome 中开始输入地址的时候, 此时实际上是 Browser 进程里的 UI 线程在处理我们的输入。首先 UI 线程就发问了, 说: "你是想要访问一个网站呢, 还是使用搜索引擎来搜索输入的内容呢?"。在 Chrome 中, 搜索框和网页导航框是同一个输入框, 所以 UI 线程首先得知道你想干嘛, 才能更好的为你服务。

第二步: 开始 Navigation
当用户按下回车时, UI 线程会召唤网络线程, 让他来帮忙获取到网页信息。这个时候, tab 角落上的 loading 就会开始旋转, 告诉用户咱已经开始加载了。然后网络线程就会开始通过适当的协议去请求网站信息, 比如 DNS 查询、为请求建立 TLS 连接 (网络栈的世界千千万, 此处不对网络栈进行深度解析, 有兴趣的话可以评论, 我们单独开篇文章来深入了解)。

但是这个时候, 网络线程可能会从服务器收到重定向的 HTTP 头比如 301。这种情况下, 网络线程会告诉 UI 线程, 说: "嘿, 服务器那头说要重定向, 你看看咋整?"。如果顺利的话, UI 线程会重新发起一个URL请求。
第三步: 读取回包
一旦请求开始响应后 (注意, 这里是流式的), 网络线程会在有必要的时候读取返回流中的前几个字节。HTTP 报文的头部 Content-Type 应该声明我们这次请求的数据类型是什么(是一个 HTML、或者是一个 JS 文件、或者图片之类的), 但是这个头部是服务器给我们返回的, 他们不一定会给这个头部甚至给的类型不一定和资源本身的类型是匹配的。服务器有可能在返回一个HTML文件的同时声明这是一张图片。MIME Type 嗅探就发生在这一步。这其实是一件很棘手的事情, 如果你有兴趣, 可以看看源码中的注释, 注释有说明不同浏览器是怎样处理 Content-Type 与资源本身类型不一致的情况。

如果回包是一个 HTML 文件, 那么下一步就是把数据传给 Renderer 进程, 但是如果回包是一个 zip 包, 或者一些其他文件的话, 那么说明这次请求是一个下载请求。数据将会被传给 download manager。

安全浏览检查也在这一步执行。如果访问的域或者返回的数据看起来是已知的恶意站点, 那么网络线程会发出警告让 Renderer 进程展示一个警告页面⚠️。此外, 还会进行 CrossOriginReadBlocking (CORB) 检查, 以确保敏感的跨站点数据不会进入到 Renderer 进程 (Renderer 进程可是执行 JavaScript 的地方)。
第四步: 找一个 Renderer 进程
一旦所有的检查都搞定了, 并且网络线程非常肯定浏览器需要导航到请求的站点时, 网络线程会告诉 UI 线程: "嘿, 我这里的数据 OK 啦!"。然后 UI 线程就会去找一个 Renderer 进程来承载网页的渲染工作。

网络线程的请求通常会花费几百毫秒, 所以浏览器为了加速这个流程做了一些优化。当 UI 线程像第二步一样发送一个 URL 请求的时候, 这个时候其实 UI 线程已经知道正在访问什么站点了, 所以 UI 线程在请求网络线程的同时, 也去准备一个 Renderer 进程(可能是复用已有的, 也有可能是新建一个)。但是这个 Renderer 进程不一定会被用上, 如果网络线程重定向到另一个站点, 那么这个 Renderer 进程就不会被用上。
第五步: 提交 Navigation
现在数据和 Renderer 进程都已经OK了, Browser 进程会发送一条 IPC 消息给 Renderer 进程来提交 navigation。同时, Browser 进程也通过数据流将数据传递给 Renderer 进程, 所以 Renderer 进程可以流式的获取 HTML 数据 (所以 Renderer 进程就有机会流式的解析HTML)。一旦 Browser 进程确认Renderer收到了提交, 那么一次 navigation 就完成了, HTML 文档开始加载。
此时地址栏会更新, 网站安全指示器也会加载(区分是否为 https )。浏览器的会话历史会更新所以前进/后退按钮也会更新到对应的状态。为了更好的从关闭中恢复tab(比如关闭了全部 tab), 浏览器会把会话历史存在磁盘里以便恢复时使用。

额外的步骤: 加载完成
一旦 navigation 被提交, Renderer 进程开始加载资源然后渲染页面。我们先跳过这中间的所有步骤, 假设现在渲染已经完成了, Renderer 进程会发送一个 IPC 消息告诉 Browser 进程渲染完成了 (发生在页面内所有的网页的 onload 被触发且被执行完之后, 包括 iframe)。这个时候, UI 线程会停止 tab 角落的旋转并切换为网页的 favicon。

Navigation to a different site
一次简单的 navigation 就完成啦!但是如果用户这个时候再次输入另一个 URL 会发生什么呢?Browser 进程会做同样的事情 navigation 到另一个 URL。但是在这之前, 他需要检查当前正在渲染的 Renderer 进程是否关心 beforeunload 事件。
beforeunload 事件类似于告诉用户"你确认要离开这个网页吗?"。一个 tab 里面所有的事情都发生在 Renderer 进程, 包括执行 JavaScript, 所以当用户发起一个新的 navigation 时 Browser 进程必须检查一下当前的 Renderer 是否需要执行 beforeunload。
⚠️ 警告 不要添加没有必要的 beforeunload 事件, 这会导致用户在 navigation 前要执行一次 beforeunload, 这就带来了一些不必要的延迟。确保只在真正需要的时候注册这个事件, 比如用户可能会丢失数据的时候。

如果从 Renderer 进程里初始化一个 navigation (比如用户点了一个链接, 或者 JavaScript 代码操作) 时, Renderer 进程首先会执行 beforeunload 事件。然后 Renderer 进程会发起 navigation 请求给 Browser 进程, Browser 进程执行和之前一样的流程。
如果 navigation 是请求不同于当前站点的域, 那么 Browser 进程会创建一个新的进程来处理新 navigation 的渲染工作, 同时老 Renderer 被保留来执行 unload 之类的事件。如果想了解更多细节, 可以看看页面生命周期状态概览。

In case of Service Worker
Service Worker 是一种可以拦截网络的 Worker, 可以帮助开发者控制请求什么时候走缓存, 什么时候走网络。如果设置了 service worker 来从缓存中加载页面, 那么就不需要从网络请求数据了。
Service Worker 是也是跑在 Renderer 进程里的 JavaScript 代码(跑在自己的 Worker 线程)。当 navigation 请求来的时候, Browser 进程怎么知道站点是否有注册 Service Worker 呢?

当 Service Worker 注册的时候, Service Worker 的 scope 会被保存为一个引用(想了解更多可以看看这篇介绍 Service Worker 生命周期的文章)。当网络线程处理 navigation 请求时, 会检查这个 URL 是否有注册 Service Worker, 如果注册了的话, UI 线程会找到一个 Renderer 进程来执行 Service Worker 代码。Service Worker 就可以从缓存中加载数据, 从而避免了从网络加载资源(这真的很慢)。

Navigation Preload
但是 Service Worker 并不一定总是返回本地缓存, 当 Service Worker 决定从网络请求资源时, 那么这套机制就拖慢了原本的流程(Browser 进程要和 Renderer 进程通信)。Navigation Preload 是一种加速这种情况的机制, 他在启动 Service Worker 的同时也并行的去加载资源。Navigation Preload 使用 HTTP 头来标记这些请求是 Preload 请求, 从而给到服务器机会对 Preload 请求返回不同的内容。

Warp-up
到此为止, 我们的 "navigation" 就结束了。这篇文章介绍了浏览器是怎样处理我们的 navigation 请求, 以及一些 H5 特性, 比如 Service Worker (如果感兴趣, 这个技术我们也可以单独出牌文章讲讲), 了解了我们的浏览器怎样获取到数据并且将其渲染出来, 相信读者现在能更好的理解一些 Web API 为什么被设计了。下一篇文章我们就来讲讲 Renderer 进程是怎样把一片文档从字节流数据渲染成页面的, 如果有任何问题, 欢迎向我提问。