作为前端,你不得不懂的歌浏览器工作原理。
你有没有思考过,
- 浏览器怎么将前端代码渲染成为可操作的页面的呢?
- 浏览器为什么会出现跨域限制呢?
- 为什么一个技术被建议用来做性能优化,其背后原理又是什么呢?
- 为什么js操作不当会阻塞页面渲染呢?
- 不同页面之前是如何通信的呢?
- 为什么有些css特性的使用会优化页面渲染性能呢?
要想解答这些,首先需要了解下计算机核心原理和Chrome多进程架构设计。
一、CPU,GPU,存储和多进程架构
1. 计算机核心-CPU GPU
CPU(Central Processing Unit)
中央处理单元,可以看作是电脑的大脑。CPU的核心,可以一个个处理很多不同的任务。过去的CPU事单个芯片,而现代CPU一般都是多核的,这样赋予计算机和手机更强大的计算能力。
GPU(Graphics Processing Unit)
图形处理单元,是计算器的另一个核心部件,它一开始设计来处理图形。与CPU不同的是,GPU擅长跨多核处理简单的任务。而近些年,GPU可以用来增强计算,并且越来越多的计算仅GPU就能完成。
当你在手机或计算机上打开一个应用时,是GPU和CPU来将应用运行起来的。通常应用在遵从操作系统的机制在CPU和GPU上运行。也即,计算机是有三层结构,底层的硬件,中间是操作系统,上层是应用。
在进程和线程上运行程序
进程,可以理解为一个应用的执行程序。
线程,则运行在进程内部,用来执行进程某一部分程序。
当启动一个应用,即启动一个进程,且操作系统会为每个进程开辟私有的内存空间,用以其工作和存储其私有的状态。而每个进程又可以在其内部创建线程来辅助其执行。当你关掉一个应用,操作系统也就会释放该内存空间。
一个进程也可以请求操作系统启动另一个进程来执行不同的任务,这样操作系统就会为新进程开辟新的内存空间。而当两个进程之间需要通信的时候,它们可以通过IPC(Inter Process Communication)来实现。很多应用也都是这么实现的,这样当某一个进程出现问题事,可以直接重启该进程,而不会影响到其他在跑的应用。
2. 浏览器架构
那么浏览器怎么应用进程和线程的呢?可以是拥有多个线程的单一进程,也可以是通过IPC来通信的多个进程和线程。这些都是浏览器的具体实现细节,也没有统一的标准,不同浏览器的实现可能会差别非常大。而这里我们仅讨论Chrome浏览器的最新架构。
谷歌浏览器在上层端有一个Browser进程,然后同时有处理浏览器不同事物的其他多个进程(Renderer进程,GPU进程,Plugin进程等),通过它们共同协作来运行。
Renderer进程会为每一个tab开辟不同的进程,一般Chrome会给一个tab开一个进程,现在极限情况下属于同一个site的不同tab也可能会共享一个进程(由于内存的限制,Chrome浏览器会限制进程数量的上限,当超过这个上限之后,就会在同一个site复用一个进程)。
那么每个进程都分别负责什么呢?
-
Browser进程:负责浏览器应用层面的功能,比如地址栏、书签、前进后退等
- UI线程 - 处理地址栏,书签等
- Network线程- 处理网络请求等
-Renderer进程:负责tab内网页的显示。每个tab会有一个独立的进程(大多数情况下)。
- 插件进程:负责插件
- GPU进程:GPU进程是与其它进程独立的,用以处理来自多个应用的请求,并且将他们绘制在同一个屏幕上。
- 其它:比如扩展进程,工具进程。
如果想看Chrome在运行的进程,可以点击任务栏的选项(上右侧...)- 更多工具 - 任务管理,同时能看到每个进程使用的CPU/内存情况。
Chrome使用多进程架构的好处
第一个好处,大部分情况下,一个tab有自己独立的进程。所以当一个tab不相应的时候,其它tab仍然可以正常使用。试想,如果所有tab都公用一个进程,那如果一个tab不响应,那所有tab都没法使用,这个体验感会非常差。
第二个好处,安全和沙箱。由于操作系统会限制不同进程之间的权利,这样浏览器就可以利用这个沙箱来隔离不同进程。比如浏览器限制Renderer进程访问任意的文件。如cookie的跨域范文限制等。
当然,多进程也有它的弊端。由于不同进程都有子集独立私有的内存,所以它们都会存在框架的一些副本,比如V8引擎。这就意味着,会耗费更大的内存空间。为了节省内存的开销,Chrome浏览器会设定进程数量的上限,而这个上限取决于计算机的内存总量和CPU。当超过这个数量,就会将属于同一个site的不同tab共享一个进程。
二、导航发生了什么
当你在导航栏输入网址,到看到网页,背后发生了什么呢?这里将探讨其背后的故事,看进程和线程之间是如何通信来完成页面的渲染的。
上面讲述了浏览器中的进程和线程的概念,我们知道tab之外的事物都是有Browser进程来完成的。Browser进程有多个线程协作,诸如UI线程-绘制浏览器上的按钮和输入框,网络线程用来处理网络请求获取数据,存储线程用来控制文件的访问等等。当我们在浏览器导航栏的输入框输入网址之后,首先将有Browser进程的UI线程来处理你的输入。
下面以一个简单的例子来讲解下具体的步骤。
第一步:处理输入
在谷歌浏览器中,地址栏同时也是输入框。所以当用户在浏览器导航栏的输入框输入网址之后,Browser进程的UI线程首先会判断:这是个URL还是个搜索查询?如果是搜索查询,将会发送到搜索引擎,否则就会去你请求URL对应的网站。
第二步:开始导航
当用户点击回车键,Browser进程的UI线程就会发起一个网络请求来获取网站的内容。这个时候你会看到tab左侧会显示加载图标,而此时网络线程就会去做DNS查询获取URL对应的IP地址,如果是HTTPS请求还会去为该请求建立TLS连接。这里网络线程可能会从服务器接收到HTTP 301重定向的响应头,这种情况下,网络线程就会和UI线程通信告知服务端请求重定向,然后就会新建一个新的URL请求。
第三步:读取返回
一旦浏览器开始接收到服务器返回的响应体,网络线程将在必要时查看流的前几个字节。响应的Content-Type头应该说明它是什么类型的数据,但是由于它可能丢失或错误,所以MIME类型嗅探在这里完成。这是一件棘手的事情,正如源代码中所注释的那样,可以阅读注释,了解不同的浏览器是如何处理内容类型/有效负载值对的。
如果响应是HTML文件,那么下一步将是将数据传递给渲染程序。但如果它是zip文件或其他文件,那么这意味着它是一个下载请求,则需要将数据传递给下载管理器。
这也是安全浏览检查发生的时机。如果该域和响应数据与已知的恶意站点相匹配,则网络线程发出警报,并显示警告页面。此外,为了确保敏感的跨站点数据不会进入渲染程序,还会进行Cross Origin Read Blocking (CORB)跨源站点读取阻塞检查。
第四步:找一个或者重新开启一个Renderer进程
一旦所有检查都完成了,Network线程确信浏览器应该导航到所请求的站点,Network线程就会告诉UI线程数据已经准备好了。UI线程然后找到一个渲染器进程来进行网页的渲染。
由于网络请求可能需要几百毫秒才能得到响应,因此应用了一种优化来加快这一过程。当UI线程在第2步向网络线程发送URL请求时,它已经知道它们要导航到哪个站点。UI线程尝试主动查找或启动与网络请求并行的渲染器进程。这样,如果一切都按照预期进行,当网络线程接收到数据时,渲染器进程已经处于备用状态。当然如果导航重定向跨站点,则可能会新开一个新的Renderer进程,而弃用此备用Renderer进程。
第五步:提交导航
现在数据和渲染进程都准备好了,一个跨进程通信IPC从Broswer进程发送到Renderer进程来提交导航。同时会传递数据流,以便渲染进程可以继续接收HTML数据。一旦浏览器进程在渲染进程中接收到确认提交已经发生,此时导航部分工作就完成了,接下来就开始文档加载阶段了。
此时,地址栏已更新,安全指示图标和站点设置UI反映新页面的站点信息。该选项卡的session历史将被更新,因此后退/前进按钮将会去往导航到的站点记录。为了方便关闭选项卡或窗口时恢复选项卡/会话,会话历史记录保存在磁盘上。
一旦提交了导航,Renderer进程将继续加载资源并渲染页面(具体将在下一篇文章中详细介绍这一阶段所发生的事情)。一旦Renderer进程“完成”渲染,它将IPC发送回浏览器进程(这是在页面中所有帧上的所有onload事件触发并完成执行之后
)。此时,UI线程停止选项卡上的加载旋转器。
之所以说“完成”,是因为在此之后,客户端JavaScript仍然可以加载额外的资源并呈现新的视图。