js获取浏览器地址_谷歌浏览器是如何工作的

本文详细介绍了谷歌浏览器的多进程架构,包括浏览器进程、渲染器进程、插件进程和GPU进程,强调了其优势在于防止单个标签页崩溃影响其他标签、增强安全性以及利用沙盒机制。此外,解释了渲染器进程如何处理网站内容,包括HTML解析、CSS样式计算、布局和绘制,以及合成线程在滚动和输入交互中的作用。最后,讨论了事件处理和性能优化策略,如合并事件和使用passive选项。
摘要由CSDN通过智能技术生成

5b3e3e3430d65d296e70155ee7dddc66.png

多进程架构


首先讲一下谷歌浏览器工作采用的架构。谷歌浏览器的架构,是采用下图所示的多进程架构。其中:

  • 浏览器进程(Browser Process): 负责与程序的其他进程进行协调,处理浏览器的地址栏、书签、后退和前进按钮,也处理浏览器不可见的特别部分,包括网络请求和文件读取。

  • 渲染器进程(Renderer Process): 控制显示网站标签内的所有内容。每个tab下将创建多个渲染器进程。谷歌浏览器试图为每个网站分配一个进程,包括iframes。

  • 插件进程(Plugin process): 控制网站内的所有插件,比如Flash。

  • GPU进程(GPU Process): 主要处理GPU任务,和其他进程隔离。0f4a2c6843b0c05c16eb685ef19efab4.png图1. 谷歌的多进程架构。在渲染器进程下显示了多个层代表chrome为每个tab执行多个渲染器进程[1]

多进程架构的好处

好处之一是: 多个渲染器进程可以有效避免一个tab崩溃影响其他tab的情况。假设您在浏览器里打开了3个网站,如果所有的tab都在一个进程上运行,那么当其中一个tab失去响应,其他所有tab也将会失去响应,这样对用户来说显然不够友好。如果每个tab都有自己的渲染器进程,各个tab之间互不影响,那么如果其中一个tab失去响应,您仍然可以访问其他tab。

好处之二是: 安全性和沙盒。Chrome的沙盒是依赖操作系统本身提供的机制实现,根据操作系统本身提供的相关API,限制进程的权限,比如让进程无法访问任何文件、无法获得用户输入等,Chrome的sandbox最大化的利用了这些限制,渲染器进程运行在Chrome的沙盒中[2]

浏览器前端界面由browser进程管理,各个网站的tab属于renderer进程管理。chrome的sandbox是将renderer进程作为防护对象。browser进程会给每个renderer进程分配资源,但这些renderer进程只能访问被分配的资源,不能访问未被分配的资源。这里资源的概念比较广泛,具体到可访问的DOM(对应内存中的对象),或抽象到origin scope等[3]

好处之三是: 进程拥有自己的私有内存空间,占用更多的内存。他们通常会包含通用基础架构的副本(如V8,chrome的JS引擎)

chrome中的服务化-节省更多内存

为了节省内存,chrome对可启动的进程数量进行了限制。这种限制取决于设备拥有的内存和CPU能力,当chrome达到限制后,它将开始从同一进程中启动同一站点的多个标签页。当chrome在强大的硬件上运行时,可以将单个服务拆分成不同的进程,以获得更高的稳定性,但如果是在资源受限的设备上,chrome会将多个不同的服务整合到一个进程中,从而节省了内存空间

单帧渲染器进程-站点隔离

chrome中的站点隔离,是指为每个跨站点iframe运行单独的渲染器进程。一个标签页对应一个渲染器进程,该进程允许跨站点iframe在单个渲染器进程中运行,并在不同站点之间共享内存空间。同源策略是web的核心安全模型,它可以确保一个站点在未经允许的情况下无法访问其他站点的数据,绕过该策略是安全攻击的主要目标。进程隔离是分离站点最有效的方法。通过Meltdown and Spectre[4],使用进程分离站点就变得清晰了。从chrome67开始,默认在桌面上开启站点隔离,因此每个标签的跨站点iframe都将获得一个单独的渲染器进程2154df680e672b01b2010f8499428c82.png图2. 站点隔离图(一个网站中的多个iframe对应多个渲染器进程)[1]

当用户在浏览器中输入URL后,发生了什么?


从浏览器进程开始

前文提到过,一个tab外的部分由浏览器进程处理。浏览器进程包含很多线程,如UI线程(绘制浏览器的按钮、输入字段),网络线程(处理网络堆栈接收从网络中获取的数据),存储线程(控制对文件等的访问)。当在地址栏中输入URL时,输入将由浏览器进程中的UI线程处理d1c4eec40fdccf5ecb9348a37cef40dd.png图3. 顶部是浏览器UI,底部是浏览器进程中的UI、网络、和存储线程[1]

导航过程


处理输入

当用户在地址栏中输入内容之后,UI线程要做的第一件事是,判断这是一个查询请求还是一个URL。在chrome中,地址栏也是一个搜索输入区域,因此UI线程需要解析并决定是将请求发送到搜索引擎还是直接打开请求的网站。

开始导航

当用户按下enter,UI线程会发起网络请求以获取网站内容。加载动画显示在选项卡的角落上,同时网络线程会通过适当的协议(如DNS寻址)并为请求建立TLS连接。此时,网络线程可能会收到服务器重定向头如HTTP 301。在这种情况下,网络线程与正在请求重定向的UI线程进行通信,然后,启动另一个URL请求

读取响应

如果响应是HTML文件,下一步是将数据传递到渲染器进程,但如果是zip文件或其他文件,则意味着是下载请求,因此需要将数据传递到下载管理器。
这也是发生安全检查的地方。如果域和响应数据与已知的恶意站点匹配,则网络线程将发出警并显示警告页面。除此之外,还会进行跨域读取阻止(CORB)检查,以确保敏感的跨站点数据不会进入渲染器进程。

查找渲染器进程

一旦完成所有检查,并且网络线程确信浏览器应导航到请求的站点,则网络线程将告知UI线程数据已准备就绪。然后UI线程查找渲染器进程进行网页渲染。
由于网络线程可能需要数百毫秒才能获得响应,因此需要优化该过程。当UI线程向网络线程发送URL请求时,它已经知道需要导航到哪个站点。因此可以将UI线程查找渲染器进程的过程和网络请求并行进行。这样,如果一切按预期进行,则当网络线程接收到数据时,渲染器进程也已经准备好了。如果导航重定向到其他站点,则可能不会使用已经准备好的渲染器进程,在这种情况下,可能需要其他进程。

提交导航

现在数据和渲染器进程都已经准备好,从浏览器进程向渲染器进程发送一个IPC以提交导航。它还会传递数据流,因此渲染器进程可以继续接收HTML数据。一旦浏览器进程接收到渲染器进程已经提交的确认,则表示导航已经完成,文档加载阶段开始。
此时,地址栏已经更新,安全指示符和网站设置UI反映了新页面的网站信息。该tab的session历史记录将被更新,因此点击后退/前进按钮将逐步浏览刚刚导航到的站点。为方便在关闭tab或窗口时恢复tab/session的内容,session历史记录将被存储在磁盘上

初始加载完成

提交导航后,渲染器进程将继续加载资源并渲染页面。渲染器进程完成渲染后,它将IPC发送回浏览器进程(这是在页面上所有帧触发完onload事件并完成执行后)。此时,UI线程在tab上停止加载动画。这里说“完成”是因为客户端js仍可以在此之后加载其他资源并呈现新视图

导航到其他站点

如果用户再次在地址栏中输入其他URL会发生什么情况?浏览器将通过相同的步骤导航到其他站点。但是,在此之前,它需要对当前渲染的站点进行检查,看看是否有beforeunload事件处理器。

当想要关闭当前tab或者导航到其他页面,beforeunload事件将被触发,可以通过监听该事件创建“确定离开此网站?”的提醒。选项卡内部的所有内容包括您的js代码都由渲染器进程处理,因此,当有新的导航请求出现时,浏览器进程需要和当前渲染器进程进行比较。

注意:不要添加没有限制条件的beforeunload处理程序。因为这样会造成延迟,需要在开始导航前处理更多程序。仅在必要情况下才添加该事件的处理程序,如当用户离开页面时,需要提醒他这样会导致在页面上输入的数据丢失

service worker

Service worker是一种在应用代码中编写代理的方法,使得web开发人员能更好的控制本地缓存内容及何时从网络中获取数据。如果service worker设置的是从缓存中读取数据,则不需要发起网络请求。需要考虑的一点是js代码中的service worker是运行在渲染器进程中的,那么当导航请求发起时,浏览器进程怎么知道这个网站有service worker?

当导航开始,网络线程检查域名是否在已经注册的service worker域名列表里。如果该url已经注册了service worker,UI线程开始为执行service worker代码寻找渲染线程。Service worker可能从缓存中加载数据,从而无需发起网络请求获得数据,也可能从网络加载新的资源。

导航预加载

如果service worker最终还是要从网络请求数据,则浏览器进程与渲染器进程之间的这种往返可能会导致延迟。导航预加载是一种通过和service worker并行加载资源来加速此过程的机制。它通过header标记这些请求,使服务器可以对这些请求响应不同的内容。例如:仅更新数据而不是完整文档

渲染器进程的内部工作

渲染器进程处理web内容

渲染器进程负责每个tab内部发生的事情。在一个渲染器进程中,主线程处理发送给用户的大部分代码。如果使用了web worker或service worker,则worker线程也会处理一部分js代码。Compositor thread(合成线程) 和raster thread(光栅线程)也运行在渲染进程内部,以高效、流畅的渲染页面。渲染器进程的核心工作是把HTML,CSS和JS转换为用户可以交互的网页

HTML解析

构建DOM树

当渲染器进程接收到来自导航提交的信息并开始接收HTML数据时,主线程开始解析文本字符串(HTML),并将其转换为文档对象类型(DOM)。在浏览器中渲染HTML永远不会引发错误。如缺少闭合的标签也是有效的HTML。这是因为HTML规范旨在优雅的处理这些错误。

子资源加载

一个网站通常使用外部资源,如图片、css和js,这些文件需要从网络中获取。主线程可以在构建DOM时碰到这些需要发起请求的资源的时候在依次发起请求,但是,为了提高页面加载速度,“预加载扫描器”会并行执行。如果HTML文档中有类似img或link之类的内容,则预加载扫描器将查看HTML解析器生成的令牌,并在浏览器进程中发送请求到网络线程。

js会阻塞解析

当HTML解析过程中碰到script标签,它会暂停HTML文档的解析,加载、解析和执行js代码。为什么?因为JS可以使用document.write改变文档的形状,参见下图。811594a2bf78df70a3e318f0c3810bd7.png

图4. HTML解析过程[5]

浏览器如何加载资源

如果js中没有使用document.write,可以为script标签添加async和defer属性,异步加载和执行js代码而不阻塞解析,也可以使用js模块,或者使用 也是一种方式来通知浏览器当前导航肯定需要该资源,需要尽快下载。

样式计算

只有DOM并不足以知道页面的外观,因为我们可以在CSS中设置页面元素的样式。主线程解析CSS并确定每个DOM节点的计算样式。
浏览器拥有一个默认样式表,即使不提供任何css,每个DOM节点也都有一个计算样式,即默认样式。如h1标签比h2标签大并且每个元素的margin都不同。

布局

布局是一个查找元素几何形状的过程。主线程遍历DOM和计算样式,并创建布局树,该树具有诸如X、Y坐标和边界框大小之类的信息。布局树可能与DOM树有相似的结构,但它仅包含和页面上可见内容相关的信息。如果使用display:none,则该元素不在布局树中(然而,如果使用visibility:hiddden,则在布局树中)。同样,如果使用了伪类,如 p::before{content:"Hi!"},即使它不在DOM中,它也包含在布局树中

绘制

主线程遍历布局树以创建绘制记录。绘制记录是绘制过程的注释,例如“先是背景,然后是文本,然后是矩形”。

更新渲染管道的成本很高

在渲染管道中要掌握的比较重要的一点是,在每个步骤中,之前操作的结果都将用于创建新数据。例如,如果布局树中发生一些更改,则需要为文档的受影响部分重新生成绘制顺序。如果要设置动画元素,则浏览器必须在帧与帧之间执行这些操作。我们的多数设备每秒刷新屏幕60次(即60帧),如果帧与帧之间移动物体,则人眼看到的动画会是平滑的。但是,如果错过了,页面就会显得有些混乱。

合成


如何绘制页面

现在浏览器知道了文档的结构,每个元素的样式,页面的几何形状及绘制顺序,那么它将如何绘制页面?将这些信息转化为屏幕上的像素称为光栅化。处理这一问题的一种简单的做法是在视口内部光栅化零件。如果用户滚动页面,则移动光栅化的框架,并通过光栅化更多内容来填充缺失的部分。这就是chrome首次发布时处理光栅化的方式。但是,现代浏览器运行着一种更复杂的过程,称为合成

什么是合成

合成是一种将页面的各个部分分成若干层,分别对其进行栅格化并在合成器线程中进行页面合成的技术。如果发生滚动,因为图层已经被光栅化,所以要做的就是合成一个新的帧。可以通过相同的方式来实现动画,移动图层并合成新的帧。

分割层

为了找出哪些元素需要位于哪个图层中,主线程需要遍历布局树以创建图层树(在DevTools性能面板中,此部分称为更新图层树)。与每帧光栅化页面的一小部分相比,合成过量的层可能会导致操作速度变慢。所以测试应用的渲染性能至关重要。debc7758c5a4e2684f26cc4e74314ad8.png图5. chrome devtools中的更新图层树

主线程的光栅和合成

一旦创建了层树并确定了绘制顺序,主线程便将这些信息提交给合成线程。然后,合成线程将每个图层光栅化。一层有可能和页面的整个长度一样大,因此合成器线程将它们划分为图块,并将每个图块发送给光栅线程。合成器线程可以优先处理不同的光栅线程,以便可以优先对视口(或附近)中的事物进行光栅化。图层还具有用与不同分辨率的多个拼贴,以处理诸如放大动作之类的事情。光栅化后,合成器线程会收集称为“draw quads”的图块信息以创建一个合成器框架。然后,合成器框架通过IPC提交给浏览器进程。此时,可以从UI线程或从其他渲染器进程添加另一个合成器框架。这些合成器框架被发送给GPU,以将其显示在屏幕上。如果发生滚动事件,则合成器线程会创建另一个合成器框架,以发送到GPU。

合成的好处是它无需涉及主线程即可完成。合成器线程无需等待样式计算或js执行。这就是为什么“仅合成动画[6]”被认为是获得最佳平滑效果的原因。如果需要重新计算布局或绘画,则必须涉及主线程

合成器如何能实现用户输入的平滑交互

从浏览器的角度看输入事件

当你听到“输入事件”可能想到的是文本输入框点击或者鼠标点击,但是从浏览器的角度,输入意味着用户的任意姿势。鼠标滚轮滚动是输入事件,触摸或鼠标悬停也是输入事件。

当发生用户手势(如屏幕上的触摸)时,浏览器进程就是首先接受手势的进程。但是,浏览器进程仅知道该手势发生的位置,因为选项卡内部的内容由渲染器进程处理。因此,浏览器进程将事件类型(如touchstart)及其坐标发送到渲染器进程。渲染器进程通过找到事件目标并运行附加的事件侦听器来适当处理事件。

合成器接收输入事件

如果页面上没有任何输入事件侦听器,则合成线程可以创建一个完全独立于主线程的新合成框架。但是,如果页面中有事件监听器怎么办?合成线程如何找出是否需要处理事件?

了解非快速滚动区域

由于执行js是主线程的工作,因此在合成页面时,合成线程会将页面上具有事件处理程序的区域标记为“非快速滚动区域”。如果事件发生在该区域中,则合成线程可以确保将输入事件发送到主线程。如果输入事件来自该区域之外,则合成线程将在不等待主线程的情况下合成新的框架。

添加事件监听程序时应该更加谨慎

我们经常会看到如下的事件委托代码

document.body.addEventListener('touchstart', event => {    if (event.target === area) {        event.preventDefault();    }});

从开发的角度看,事件委托可以减少代码体积。但是,从浏览器的角度看这段代码,现在整个页面都被标记为了非快速滚动区域。这意味着,即使应用程序不关心页面某些部分的输入,合成线程也必须与主线程进行通信,并在每次输入事件发生时等待它。因此,破坏了合成器的平滑滚动能力。

为了减少这种情况的发生,可以在事件侦听器中传递passive:true选项。该选项告诉浏览器在主线程中侦听事件的同时,合成器仍可以继续合成新的框架

document.body.addEventListener('touchstart', event => {    if (event.target === area) {        event.preventDefault()    } }, {passive: true});

检查事件是否可取消

假设页面中有一个框,只在水平方向滚动。在指针事件中使用passive:true意味着页面平滑滚动,但是垂直滚动可能在使用preventDefault限制滚动方向时已经开始。可以使用event.cancelable方法对此进行检查

document.body.addEventListener('pointermove', event => {    if (event.cancelable) {        event.preventDefault(); // block the native scroll        /*        *  do what you want the application to do here        */    }}, {passive: true});

也可以使用css规则如touch-action来完全消除事件处理程序

#area {  touch-action: pan-x;}

寻找事件目标

当合成线程向主线程发送输入事件时,要做的第一件事就是使用命中测试寻找事件目标。命中测试是指利用在绘制过程中生成的绘制记录数据来找到事件发生的点坐标下的内容

最小化事件分配到主线程

如果一个连续的事件像touchmove每秒发送到主线程120次,则与屏幕刷新速度相比,它可能会触发大量的点击测试和js执行,见下图4410d012fcd19a61315a0bbd5644045b.png图6. 事件淹没了帧时间轴导致页面混乱[1]为了最大程度的减少对主线程的过度调用,chrome会合并连续事件(例如wheelmousewheelmousemovepointermovetouchmove),并将调度延迟到下一个requestAnimationFrame之前283e91bdc3a715c4eda39f850a5e03ea.png图7. 和上图同样的时间轴,但事件已合并和延迟[1]任何离散事件(如keydownkeyupmouseupmousedowntouchstarttouchend)都将立即派发

使用getCoalescedEvents获取帧内事件

对于大多数的web应用,合并事件可以提供良好的用户体验。但是,如果是在绘制应用和基于touchmove坐标放置路径等的场景,为了绘制平滑曲线则可能会丢失中间的坐标。在这种情况下,可以在指针事件中使用getCoalescedEvents方法来获取有关合并事件的信息7c81ccd4c793ad760961779479d46c52.png图8. 左侧是平滑的触摸手势路径,右侧是有限合并路径[1]

window.addEventListener('pointermove', event => {    const events = event.getCoalescedEvents();    for (let event of events) {        const x = event.pageX;        const y = event.pageY;        // draw a line using x and y coordinates.    }});

总结


本文主要从以下几个方面阐述了谷歌浏览器的内部工作机制:

  1. 浏览器采用的架构及为什么采用这样的架构?

  2. 用户在地址栏中输入内容之后,浏览器的解析及导航过程

  3. 从服务器拿到网站的HTML之后,如何进行渲染?

  4. 网页如何实现的用户可交互?

希望能对您有所帮助!

b0879aa09101374fcabe85c007a63ccb.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值