当前位置: 首页 > Web前端 > JavaScript

【7000字】一晚上浏览器从输入到渲染的原理

时间:2023-03-27 00:54:15 JavaScript

前言Chrome漫画,简单介绍Chrome架构的漫画,2008年和Chrome浏览器一起发布的。虽然Chrome已经发布了十多年来,[Chrome漫画]漫画中介绍的核心原理对理解Electron仍然有帮助。(原文,中文)漫画目录如下:开源浏览器背后的故事稳定、严格和多任务架构速度:Webkit和V8搜索和用户体验安全、沙盒模式和无危险浏览Gears、标准和开源代码1.CPU、GPU内存和多进程架构计算机的核心是CPU和GPUCPUCPU是计算机的大脑,可以处理很多不同的任务。大多数CPU都是单芯片。一个核心相当于同一芯片中的另一个CPU。GPUGPU最初是为图形处理而开发的,擅长同时跨多个CPU处理简单任务。通常,应用程序使用操作系统提供的机制在CPU和GPU上运行。进程和线程进程可以描述为应用程序的执行程序,而线程是存在于进程内部并执行其进程程序的任何部分的线程。一个程序在启动时会创建一个进程,程序也可能会创建线程来帮助它工作。操作系统提供了一个“内存块”供进程使用,所有的应用程序状态都保存在这个私有内存空间中。当应用程序关闭时,进程也随之消失,操作系统释放内存。一个进程可以要求操作系统启动另一个进程来运行不同的任务,此时会为新进程分配不同的内存部分。如果两个进程需要聊天,就需要IPC。如果一个工作进程变得没有响应,它可以在不停止运行应用程序不同部分的其他进程的情况下重新启动。浏览器架构对于浏览器来说,一个进程可以有多个不同的线程,或者多个不同的进程可以有多个线程通过IPC进行通信。对于Chrome浏览器,最新的架构如下:进程充当Browser浏览器进程,控制应用程序的“chrome”部分,包括地址栏、书签、后退和前进按钮。还处理Web浏览器的不可见的特权部分,例如网络请求和文件访问。呈现器控制网站选项卡内任何内容显示的呈现器进程。插件进程,它控制网站使用的任何插件,例如flash。GPU图形处理进程独立于其他进程处理GPU任务。它被分成不同的进程,因为GPU处理来自多个应用程序的请求并将它们绘制在同一表面上。下图是不同的进程指向浏览器UI的不同部分:当然还有更多的进程,比如extension进程,utility进程等等。Chrome中多处理的优势假定打开了三个选项卡,每个选项卡都由单独的渲染器进程运行。如果选项卡变得无响应,您可以关闭无响应的选项卡并继续工作,同时保持其他选项卡处于活动状态。如果所有选项卡都在一个进程上运行,当一个选项卡变得无响应时,所有选项卡都会变得无响应。将浏览器的工作拆分为多个进程的另一个好处是安全性和沙盒。由于操作系统提供了一种限制进程权限的方法,因此浏览器可以将某些进程从某些功能中沙箱化。例如,Chrome浏览器将任意文件访问限制为处理任意用户输入的进程,例如渲染器进程。因为进程有自己的私有内存空间,它们通常包含公共基础设施的副本(例如V8,它是Chrome的JavaScript引擎)。这意味着更多的内存使用,因为它们不能像它们是同一进程中的线程那样共享。为了节省内存,Chrome限制了它可以启动的进程数。限制取决于设备的内存和CPU能力,但当Chrome达到限制时,它会开始在一个进程中运行来自同一站点的多个选项卡。更多内存节省——在Chrome中为Chrome提供服务Chrome正在经历架构更改,以将浏览器程序的每个部分作为服务运行,这些服务可以轻松地拆分为单独的进程或聚合为单个进程。当Chrome在强大的硬件上运行时,它可能会将每个服务拆分到不同的进程中以提供更高的稳定性,但如果它在资源受限的设备上运行,Chrome会将服务合并到一个进程中以节省内存使用。在此更改之前,类似的方法已在Android等平台上使用,以合并进程以减少内存使用。站点隔离站点隔离为每个跨站点iframe运行一个单独的渲染器进程,并在不同站点之间共享内存空间。同源策略是网络的核心安全模型,它确保一个站点不能在未经他们同意的情况下访问其他站点的数据。对于攻击者来说,绕过同源策略是安全攻击的主要目标,而对于浏览器来说,需要使用进程来分隔站点。从Chrome67开始,桌面端默认启用站点隔离,标签页中的每个跨站点iframe都有一个单独的渲染器进程,当然,从根本上改变了iframe之间的通信方式。2、导航跳转在浏览器中写入一个URL,然后浏览器从网上获取数据,显示一个页面。请求站点和浏览器在渲染之前做了什么?前面我们知道,tab之外的所有内容都是由浏览器进程处理的,也就是BrowserProcess。浏览器中的进程具有绘制按钮和输入的UI线程、处理网络堆栈以从Internet接收数据的网络线程、控制文件访问的存储线程等线程。当在地址栏中输入URL时,输入由浏览器进程的UI线程处理。入门第1步:处理输入当在地址栏中输入内容时,UI线程首先询问的是“这是搜索查询还是URL?”。在Chrome中,地址栏也是一个搜索输入框,因此UI线程需要解析它并决定是将其发送到搜索引擎,还是发送到请求的站点。第2步:开始查找当按下Enter键时,UI线程发起网络请求以获取站点内容。Loading微调器显示在选项卡的一角,网络线程通过适当的协议,例如DNS查找和为请求建立TLS连接。此时,网络线程可能会收到服务器重定向头,例如HTTP301。此时,网络线程与服务器请求重定向的UI线程进行通信。然后,将发出另一个URL请求。第3步:读取响应一旦响应的开头出现,即请求的有效负载,网络线程会在必要时查看流的前几个字节。响应的Content-Type标头应该说明它是什么类型的数据,但由于它可能丢失或错误,因此在这里完成MIME类型验证。如果响应是一个HTML文件,那么下一步就是将数据传递给GPU进程,但是如果它是一个zip文件或其他一些文件,那么它就是一个下载请求,然后他们需要将数据传递给下载经理。它也是执行安全浏览检查的地方。如果域和相应的数据匹配到恶意网站,网络线程将发送警报并显示警告页面。在此过程中还会进行CORS检查,以确保不会将敏感的跨站数据丢给Renderer。第四步:找到渲染器进程一旦完成所有检查并且网络线程确定浏览器应该导航到请求的站点,网络线程就会告诉UI线程数据已准备就绪。然后UI线程找到渲染器进程来渲染网页。由于网络请求可能需要数百毫秒才能获得响应,因此应用优化来加速该过程。当UI线程在第2步中向网络线程发送URL请求时,它已经知道他们想要导航到哪个站点。UI线程尝试与网络请求并行地主动查找或启动渲染器进程。这样,如果一切按预期进行,渲染器进程在网络线程接收到数据时已经处于待命状态。如果导航跨站点重定向,则可能不会使用此替代过程,在这种情况下可能需要不同的过程。第5步:提交现在数据和呈现器进程已准备就绪,IPC从浏览器进程发送到呈现器进程以提交导航。它还传递数据流,因此呈现器进程可以继续接收HTML数据。一旦浏览器进程听到渲染器进程中发生提交的确认,导航就完成了,文档渲染阶段开始了。此时,地址栏已经更新,安全指示器和站点设置UI反映了新页面的站点信息。该选项卡的会话历史记录将更新,因此后退/前进按钮将逐步浏览您刚刚导航到的站点。为了在关闭选项卡或窗口时促进选项卡/会话恢复,会话历史记录存储在磁盘上。其他步骤提交后,renderer进程会继续加载资源,渲染页面。当渲染器进程“完成”渲染时,它会将IPC发送回浏览器进程(这是在页面加载中的所有帧上触发所有事件并完成执行之后)。此时,UI线程停止在选项卡上加载小负载。在这一点之后,客户端JavaScript仍然可以加载额外的资源并呈现新的视图。导航到其他站点如果用户再次将不同的URL放入地址栏会怎样?浏览器进程通过相同的步骤导航到不同的站点。但在此之前,它需要检查当前呈现的站点是否有beforeunload事件。beforeunload可以创建一个“离开这个站点?”事件,该事件在左或关闭选项卡时提醒。选项卡内的所有内容,包括JavaScript代码,都由渲染器进程处理,因此当有新的导航请求进入时,浏览器进程必须检查当前的渲染器进程。注意:不要添加无条件的beforeunload处理程序。它会导致更多延迟,因为处理程序需要在导航开始之前执行。只应在必要时添加此事件处理程序,例如,如果需要警告用户他们可能会丢失在页面上输入的数据。当新导航到达与当前呈现的站点不同的站点时,将调用单独的呈现进程来处理新导航,同时保持当前呈现进程处理诸如卸载之类的事情。有关页面生命周期状态的信息,请参见此处。下图是从浏览器进程到新渲染器进程的2个IPC,告诉页面渲染,告诉旧渲染器进程卸载:从网络中检索得到新的数据。如果服务工作者设置为从缓存加载页面,则无需从网络请求数据。注意:ServiceWorker是在渲染器进程中运行的JavaScript代码。但是当导航请求进来时,浏览器进程如何知道哪个站点有ServiceWorker?注册ServiceWorker后,ServiceWorker的范围将被保留。发生导航时,网络线程会根据已注册的服务工作人员范围检查域,如果为URL注册了服务工作人员,则UI线程会查找渲染器进程来执行服务工作人员代码。ServiceWorker可能会从缓存中加载数据,这样它就不需要从网络请求数据,或者它可能会从网络请求新资源。下图是浏览器进程中的UI线程启动了renderer进程来处理serviceworker;渲染器进程中的工作线程然后从网络请求数据:导航预加载如果服务工作线程最终决定从网络请求数据,则浏览器进程和渲染器进程之间的这种往返可能会导致延迟。导航预加载是一种通过在ServiceWorker启动的同时加载资源来加速此过程的机制。它用标头标记这些请求,允许服务器决定为这些请求发送不同的内容;例如,只更新数据而不是完整的文档。3.渲染导航后,浏览器会调用渲染器(UI)进程进行工作。渲染器进程处理Web渲染器进程负责选项卡内发生的所有事情。在渲染器进程中,主线程处理大部分发送给用户的代码。如果使用WebWorker或ServiceWorker,一些JavaScript由工作线程处理。合成器和光栅器线程也在渲染器进程中运行,以高效、流畅地渲染页面。渲染器进程的核心工作是将HTML、CSS和JavaScript转换成用户可以与之交互的网页。解析和构建DOM当渲染进程接收到用于导航的提交消息并开始接收HTML数据时,主线程开始解析HTML并将其制作成DOM。DOM是浏览器对页面的内部表示,是开发人员可以通过JavaScript与之交互的数据结构和API。将HTML文档解析成DOM是HTML标准定义的,所以有时错误的标签会被自动更正。有关详细信息,请参阅解析器中的错误处理。子资源加载对于图像、CSS、JavaScript等外部资源,需要从网络或缓存中加载。主线程在解析和构建DOM的过程中,可以找到并一一请求,但是为了加快速度,“preloadscanner”并发运行。如果HTML文档中有类似的东西,预加载扫描器会查看HTML解析器生成的令牌,并将请求发送到浏览器进程中的网络线程。JavaScript可以阻止解析当HTML解析器找到标签中添加async或defer属性。然后浏览器异步加载和运行JavaScript代码,并且不会阻止解析。如果浏览器支持,当然你也可以使用JavascriptModule。是告知浏览器当前导航肯定需要该资源并希望尽快下载的一种方式,这里是资源优先级。样式解析主线程解析CSS并确定每个DOM节点的计算样式。每个DOM节点都有一个默认样式,即默认样式表。布局至此,渲染器进程知道文档的结构和每个节点的样式。布局是寻找元素几何形状的过程,主线程遍历DOM并计算样式并创建布局树,其中包含xy坐标和边界框大小等信息。布局树在结构上可能类似于DOM树,但它仅包含与页面上可见内容相关的信息。如果display:none被应用,该元素不是布局树的一部分(但是,visibility:hidden它是)。类似地,如果应用了一个内容如p::before{content:"Hi!"}的伪类,即使它不在DOM中,它也会被包含在布局树中。CSS代表整个页面的初始布局。如果您想了解更多,请阅读这篇演讲!绘图至此,我们有了DOM、样式和布局,但是我们需要判断绘图的顺序才能开始绘图。例如,某些元素可能设置了z-index,这样的话按照HTML中写的顺序绘制元素会导致渲染不正确。在绘制步骤中,主线程遍历布局树以创建绘制记录。绘图记录的顺序是:先背景,再文字,再矩形。这有点像的绘制过程。绘图过程中最需要注意的一点是,绘图的每一步都使用前一个操作的结果来创建新的数据。如果布局树中的某些内容发生更改,则需要为文档的受影响部分重新生成绘制顺序。如果您为元素设置动画,浏览器必须在每一帧之间运行这些操作。我们的大多数显示器每秒刷新屏幕60次(60fps);当对象每帧在屏幕上移动时,动画对人眼来说看起来很流畅。但是,如果动画错过了中间的一帧,页面就会出现“卡顿”。尽管渲染操作跟上屏幕刷新,但这些计算是在主线程上执行的,这意味着当应用程序运行JavaScript时,它可能会被阻塞。这时候可以将JavaScript操作分成小块使用requestAnimationFrame()来处理,也可以通过WebWorker来运行JavaScript,避免阻塞主线程。JS执行优化,可以点击这里。合成光栅化到现在为止,浏览器知道了文档的结构,每个元素的样式,页面的几何形状,绘制顺序,开始做真正的绘制。将此过程转换为屏幕上的像素称为光栅化。当Chrome首次发布时,它处理光栅化的方式是只光栅化视口内的部分页面,当用户滚动页面时,它移动光栅的架子并用更多的光栅填充缺失的部分。但是,现代浏览器中运行着一个更复杂的过程,称为合成。合成,将页面的部分拆分为层,单独光栅化它们,然后在与合成器线程不同的线程中将它们合并到单个页面中。此时如果发生滚动,因为图层已经光栅化,它所要做的就是合成一个新的框架。可以通过移动层和合成新帧以相同的方式实现动画。要查看页面的图层,您可以从控制台中的更多工具-->图层打开它。分层为了弄清楚哪些元素需要在哪些层中,主线程遍历布局树以创建一个层树(在DevTools的性能面板中可以称为“更新层树”)。如果页面中应该是单独层的部分(例如滑入式侧边菜单)没有被提取,可以使用CSS中的will-change属性通知浏览器。与每帧光栅化页面的一小部分相比,为每个元素提供图层和合成会导致操作缓慢。在主线程上进行光栅化和合成一旦创建了层树并确定了绘制顺序,主线程就会将此信息提交给合成器线程。然后合成器线程光栅化每一层。图层可以与页面的整个长度一样大,因此合成器线程将它们分解成小块并将每个小块发送到光栅线程。栅格线程栅格化每个图块并将它们存储在GPU内存中。合成器线程可以优先考虑不同的光栅线程,以便首先光栅化视口中(或附近)的事物。一个层也有多个不同分辨率的图块来处理诸如放大之类的事情。在图块被栅格化之后,合成器线程收集称为绘制四边形的图块信息来创建合成器框架。顾名思义,合成器框架表示页面框架的绘制四边形集合。绘制四边形包含诸如图块在内存中的位置以及在页面中绘制图块的位置等信息(考虑到页面组成)。合成器框架然后通过IPC提交给浏览器进程。此时,可以从用于浏览器UI更改的UI线程或用于扩展的另一个渲染器进程添加另一个合成器框架。这些合成器帧被发送到GPU以在屏幕上显示它们。如果发生滚动事件,合成器线程会创建另一个合成器帧以发送到GPU。合成的好处在于它是在不涉及主线程的情况下完成的。合成器线程不需要等待样式计算或JavaScript执行。这就是为什么只合成动画被认为是流畅性能的最佳选择。如果需要重新计算layout或者paint,就必须涉及到主线程。用户输入和合成器浏览器输入事件从浏览器的角度来看,输入意味着来自用户的任何事件。鼠标滚轮滚动是一个事件,触摸或鼠标悬停也是如此。当用户在屏幕上做出触摸等手势时,浏览器进程首先接收到该手势。但是,浏览器进程只知道手势发生的位置,因为选项卡内的内容由渲染器进程处理。因此,浏览器进程将事件类型(如touchstart)及其坐标发送给渲染器进程。渲染器进程通过查找事件目标并运行附加的事件侦听器来适当地处理事件。下图显示了Input事件如何通过浏览器进程路由到渲染器进程:非快速滚动区域。如果没有附加到页面的输入事件侦听器,合成器线程可以创建一个完全独立于主线程的新合成框架。将侦听器附加到页面后,合成器线程如何确定是否需要处理事件?由于运行JavaScript是主线程的工作,因此在合成页面时,合成器线程将页面上附加有事件处理程序的区域标记为“不可快速滚动”。通过获得此信息,合成器线程可以确保在该区域中发生事件时将输入事件发送到主线程。如果输入事件来自该区域之外,则合成器线程会继续合成新帧,而无需等待主线程。事件委托开发中常见的事件处理模式是事件委托。由于事件冒泡,可以在最顶层的元素上附加一个事件处理程序,并根据事件目标委托任务,例如:document.body.addEventListener('touchstart',event=>{if(event.target===area){event.preventDefault();}});如果您需要为所有元素编写单个事件处理程序,则此事件委托模式很有吸引力。但是,如果从浏览器的角度来看这段代码,整个页面现在都被标记为非快速滚动区域。这意味着即使程序不关心来自页面某些部分的输入,合成器线程也必须与主线程通信并在每次输入事件进来时等待它。因此,合成器的平滑滚动能力是打败了。为了减少这种情况的发生,可以传递属性passive:true,它会提示浏览器你仍然想在主线程中监听事件,但是合成器也可以继续合成新的帧,比如as: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();//阻塞nativescroll/**在这里做你想让应用程序做的事*/}},{passive:true});或者,可以使用CSS规则touch-action完全消除事件处理程序,例如:#area{touch-action:pan-x;}寻找event.target当合成器线程向主线程发送输入事件时,首先要运行的是点击以找到事件目标。Hit利用渲染过程中产生的绘制记录数据,找出事件发生点坐标下方的内容。尽量减少主线程之前的事件调度知道一个典型的监视器每秒刷新屏幕60次,我们需要跟上节奏以获得流畅的动画。并用于输入。一个典型的触摸屏设备每秒提供60-120个触摸事件,一个典型的鼠标每秒提供100个事件。输入事件的保真度高于我们的屏幕刷新率。如果像touchmove这样的连续事件每秒发送120次到主线程,与屏幕刷新的速度相比,它可能会触发太多的点击和JavaScript执行:为了尽量减少对主线程的过度调用,Chrome将合并连续事件(如滚轮、mousewheel、mousemove、pointermove、touchmove)并延迟调度,直到下一个requestAnimationFrame。可以发现时间线是一样的,只是事件合并延迟了。keydown、keyup、mouseup、mousedown、touchstart和touchend等类似事件会立即执行。使用getCoalescedEvents获取帧内事件对于大多数Web应用程序,合并事件应该足以提供良好的用户体验。但是,像构建绘图程序和基于touchmove坐标放置路径这样的事情在绘制流畅的线条时可能会丢失中间坐标。在这种情况下,您可以在指针事件中使用getCoalescedEvents方法来获取有关这些合并事件的信息。下图左边是平滑触摸手势路径,右边是组合限定路径:window.addEventListener('pointermove',event=>{constevents=event.getCoalescedEvents();for(leteventofevents){constx=event.pageX;consty=event.pageY;//使用x和y坐标画一条线。}});参考资料深入了解现代网络浏览器