Insidelookatmodernwebbrowser是介绍浏览器实现原理的系列文章。共有4篇文章。本次精读介绍第二篇文章。概述本文重点介绍浏览器的路由跳转后会发生什么,下一篇将介绍浏览器的渲染过程如何渲染网页,环环相扣。上一篇介绍过,浏览器进程包括UI线程、网络线程和存储线程。当我们在浏览器菜单栏中输入网址并回车时,这组动作由浏览器进程的UI线程响应。下面根据几种不同的路由跳转场景,分别介绍内部流程。普通跳转的第一步,UI线程响应输入,判断是否是合法的URL。当然,输入也可能是一个搜索协议,这会导致分发到另一个服务进行处理。第二步,如果第一步输入的是有效的URL,UI线程会通知网络线程获取网页内容,网络线程会寻找合适的协议来处理网络请求,通常是寻址通过DNS协议并通过TLS协议建立安全链接。如果服务器返回诸如301重定向信息,网络线程会将此信息通知给UI线程,并重新开始第二步。第三步,读取响应内容。在这一步中,网络线程首先会读取头部中的一些字节,也就是我们常说的响应头部,其中包含Content-Type来告诉返回的内容是什么。如果返回的内容是HTML,网络线程会将数据传递给渲染器进程。此步骤还检查安全性,例如CORB或跨站点问题。第四步,找到渲染器进程。一旦所有检查完成,网络线程会通知UI线程准备跳转(注意此时数据还没有加载完,第三步只检查第一个字节),UI线程会通知要渲染的实际渲染器进程。为了提高性能,UI线程会在通知网络线程的同时实例化一个renderer进程。一旦网络线程完成,就可以立即进入渲染阶段。如果检查失败,则丢弃预先实例化的渲染器进程。第五步,确认导航。第四步之后,浏览器进程通过IPC向渲染器进程传输流(精读《web streams》)数据。此时导航会被确认,浏览器的各种状态(如导航状态、前进后退历史)都会被修改。同时为了方便标签页关闭后的快速恢复,会话记录会保存在硬盘上。附加步骤,加载完成。当渲染器进程加载完成时(下一篇文章会解释它是干什么的),浏览器进程的onLoad事件会被通知。此时浏览器完成最终加载状态,加载圈会消失,并触发各种onLoad回调。注意此时js可能会继续加载远程资源,但这都是加载完成后的状态。跳转到另一个网站当你要跳转到另一个网站时,在执行正常的跳转过程之前,它也会响应beforeunload事件。这个事件是在renderer进程中注册的,所以浏览器进程需要检查renderer进程是否注册了这个response。注册beforeunload无论如何都会减慢关闭选项卡的速度,所以不要不必要地注册它。如果跳转是js发送的,那么执行跳转是由renderer进程触发并由browser进程执行,后面的流程就是正常的跳转流程。需要注意的是,在执行跳转时,会触发原网站unload(网页生命周期)等事件,所以这会由旧的renderer进程响应,新网站会创建新的renderer进程。当所有旧网页关闭时,旧的渲染器进程将被销毁。也就是说,即使只有一个tab,跳转的时候短时间内也可能有多个renderer进程。ServiceWorkerServiceWorker可以在页面加载之前执行一些逻辑,甚至可以改变网页的内容,但是浏览器仍然在渲染器进程中实现了ServiceWorker。当ServiceWorker被注册时,它会被扔到一个范围内。当UI线程执行时,它会检查ServiceWorker是否在这个范围内注册。如果有,网络线程会创建一个renderer进程来执行ServiceWorker(因为是js代码)。然后网络响应由ServiceWorker接管。但是这样会慢一步,所以UI线程在注册ServiceWorker的时候往往会告诉网络线程发送一个请求。这就是NavigationPreload机制。本文介绍了网页跳转时发生的步骤,涉及到浏览器进程、UI线程、网络线程、渲染器进程的协作。精读之后,你可能会有疑问,为什么是rendererprocess而不是rendererthread呢?因为相对于进程(process)和线程(thread),它们之间的数据是被操作系统隔离的,这样网页就无法相互读取数据(mysite.com读取的是你在baidu.com上输入的账号密码),browse浏览器必须为每个tab创建一个独立的进程,甚至每个iframe都必须是一个独立的进程。看完第二篇,你应该能更深刻地感受到模块之间合理分工的重要性。UI线程处理浏览器UI的显示和用户交互,如当前加载状态变化、历史前进后退、浏览器地址栏的输入、验证和监听,以及回车等事件,但不涉及事件如如发送请求和解析网页内容、渲染等。网络线程也只处理网络相关的事情。它主要关心通信协议和安全协议。目的是快速准确地找到网站服务器并读取其内容。网络线程会读取contentheader做一些预判。读取的内容和渲染器进程有一些重叠,但是网络线程读取内容头只是为了判断内容类型,这样就可以交给渲染引擎或者下载管理器(比如zip文件),所以为了不让渲染引擎知道下载管理器的存在,读取内容头必须由网络线程来完成。与渲染器进程的通信也是由浏览器进程完成的,即UI线程和网络线程一旦要创建或与渲染器进程通信,都会交由其所在的浏览器进程处理。渲染器进程只处理渲染逻辑。它不关心它来自哪里,比如网络请求,或者被ServiceWorker拦截后修改,也不关心当前浏览器状态是什么。它只是遵循约定的接口规范,在指定节点抛出回调,其他相关模块负责修改应用状态。比如触发onLoad回调后,浏览器进程处理浏览器状态就是一个例子。再比如在渲染器进程中点击了一个新的跳转链接。这发生在渲染器进程中,但它会交给浏览器进程。因为每个模块都是完全解耦的,所以你可以找到一个可以应对任何复杂工作的模块。一个模块,而这个模块只需要处理一部分这个复杂的工作,剩下的可以交给其他模块。这就是大规模应用维护的秘诀。因此,在浏览器运行周期中,有着非常清晰的逻辑链接。这些模块必须提前规划和设计。很难想象,这些模块之间的分工是在开发中逐渐形成的。最后,在加速优化方面,Chrome惯用的手法是以资源换时间。也就是说,与其让事情尽可能并发,不如浪费潜在的资源,这一点从渲染器进程的提前创建和网络进程的提前启动就可以看出来。总结深入理解现代浏览器II介绍了网页跳转时发生了什么,以及浏览器进程和渲染器进程是如何配合的。或许这篇文章可以帮助你解答“我们来说说在浏览器地址栏输入www.baidu.com并回车后发生了什么!”/weekly如果想参与讨论,点这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号
