浏览器多进程到JS单线程(上)

本文介绍了浏览器的多进程架构,包括Browser进程、GPU进程、渲染进程、网络进程和插件进程,强调了浏览器内核(渲染进程)的重要角色。浏览器多进程带来稳定性和资源管理的优势,但也存在资源占用增加的问题。文章详细阐述了从用户输入URL到页面显示的过程,涉及GUI渲染线程与JS引擎线程的互斥关系,以及WebWorker的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

  • 进程和线程(上)* 浏览器是多进程(上)* 浏览器都包含哪些进程* 浏览器多进程的优势和问题* 浏览器内核(渲染进程)* Browser进程和浏览器内核(Renderer进程)的通信过程* 输入URL到页面显示,经历了什么* 浏览器内核中线程之间的关系(上)* GUI渲染线程与JS引擎线程互斥* WebWorker与SharedWorker* 浏览器中的JavaScript执行机制(中)* V8工作原理(中)* 从Event Loop谈JS的运行机制(下)* 事件循环进阶:macrotask与microtask(下)* 参考资料(下)进程和线程

线程是不能单独存在的,它是由进程来启动和管理的;线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。

一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。

1.进程中的任意一线程执行出错,都会导致整个进程的崩溃。
2.线程之间共享进程中的数据。
3.当一个进程关闭之后,操作系统会回收进程所占用的内存。
4.进程之间的内容相互隔离。

区别

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

浏览器是多进程的

浏览器是多进程的

浏览器都包含哪些进程?

目前Chrome 多进程架构:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。

(为了简化理解,仅列举主要进程)

1.Browser进程:浏览器的主进程;主要负责界面显示、用户交互、子进程管理,同时提供存储等功能(协调、主控);只有一个。* 负责浏览器界面显示,与用户交互。如前进,后退等* 负责各个页面的管理,创建和销毁其他进程* 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上* 网络资源的管理,下载等
2.GPU进程:最多一个,用于3D绘制等。3.渲染进程:(浏览器内核)(Renderer进程,内部是多线程的)默认每个Tab页面一个进程,都是运行在沙箱模式下,互不影响。* 将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页* 排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中
4.网络进程:主要负责页面的网络资源加载。5.第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建。因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。### 浏览器多进程的优势和问题

优点

  • 避免单个page crash、第三方插件crash影响整个浏览器
  • 多进程充分利用多核优势
  • 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性

缺点

  • 更高的资源占用。因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。
  • 更复杂的体系架构。浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。

浏览器内核(渲染进程)

1.GUI渲染线程* 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。* 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行* 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
2.JS引擎线程* 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)* JS引擎线程负责解析Javascript脚本,运行代码。* JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序* 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
3.事件触发线程* 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)* 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中* 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理* 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
4.定时触发器线程* 传说中的setIntervalsetTimeout所在线程* 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)* 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)* 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
5.异步http请求线程* 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求* 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

Browser进程和浏览器内核(Renderer进程)的通信过程

输入URL

  • 首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。
  • 然后,在网络进程中发起真正的 URL 请求。
  • 接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
  • 浏览器进程接收到网络进程的响应头数据之后,发送“提交导航 (CommitNavigation)”消息到渲染进程;
  • 渲染进程接收到“提交导航”的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道;
  • 最后渲染进程会向浏览器进程“确认提交”,这是告诉浏览器进程:“已经准备好接受和解析页面数据了”。
  • 浏览器进程接收到渲染进程“提交文档”的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。

页面展示

1. 用户输入

当用户在地址栏中输入一个查询关键词回车后,浏览器有给当前页面一次执行 beforeunload 事件的机会,beforeunload 事件允许页面在退出之前执行一些数据清理操作,还可以询问用户是否要离开当前页面,比如当前页面可能有未提交完成的表单等情况,因此用户可以通过 beforeunload 事件来取消导航,让浏览器不再执行任何后续工作。

2. URL 请求过程
  • 浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网络进程接收到 URL 请求。
  • 网络进程首先,会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行 DNS 解析,以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。
  • 接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。
  • 服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。* 重定向:如果发现返回的状态码是 301、302、303、307、308,那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求。> 301 Moved Permanently 永久重定向:在请求的 URL 已被移除时使用。响应的 Location 首部中应该包含 资源现在所处的 URL。> 302 Found 临时重定向:与 301 状态码类似;但是,客户端应该使用 Location 首部给出的 URL 来临时定位资源;302默认以get重新发出请求(eg 以post访问 a.com ,用302重定向到b.com,浏览器会使用get请求b.com。但这样就会导致之前的post请求数据丢失,相对的 307不允许修改请求方法,这也是302和307最大的区别)> 307 Temporary Redirect:和302相似,有一个唯一的区别是不允许将请求方法从post改为get。> 308 Permanent Redirect 永久重定向:此状态码类似于301(永久移动),但不允许更改从POST到GET的请求方法。* 响应数据类型处理:Content-Type 是 HTTP 头中一个非常重要的字段, 它告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据 Content-Type 的值来决定如何显示响应体的内容。如果 Content-Type 字段的值被浏览器判断为下载类型application/octet-stream,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是 HTMLtext/html,那么浏览器则会继续进行导航流程
3. 准备渲染进程

Chrome 的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点(相同的协议和根域名)的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫 process-per-site-instance

4. 提交文档
  • 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
  • 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
  • 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
  • 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
5. 构建 DOM 树(Dom)

浏览器无法直接理解和使用 HTML,所以渲染进程首先将 HTML 解析为浏览器可以理解的 DOM

6. 样式计算(Style)
  • 渲染进程把CSS转换为浏览器能够理解的结构(当渲染进程接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets);
  • 转换样式表中的属性值,使其标准化(需要将所有值转换为渲染进程容易理解的、标准化的计算值);
  • 计算出 DOM 树中每个节点的具体样式(涉及到 CSS 的继承规则和层叠规则了:CSS 继承就是每个 DOM 节点都包含有父节点的样式;层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法)
7. 布局阶段(Layout)

创建布局树;计算每个元素的几何坐标位置,并将这些信息保存在布局树中

8. 分层阶段(layer)

为了更加方便地实现一些效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index做 z 轴排序等,渲染进程还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)

  • Chrome 的“开发者工具”的“Layers”标签,就可以可视化页面的分层情况
  • 并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层
  • 通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层:* 拥有层叠上下文属性的元素会被提升为单独的一层;* 需要剪裁(clip)的地方也会被创建为图层;出现滚动条,滚动条也会被提升为单独的层

上面内容描述的为渲染图层,下述渲染图层 复合图层(硬件加速)区别与联系

  • 渲染图层:是页面普通的文档流。我们虽然可以通过绝对定位,相对定位,浮动定位脱离文档流,但它仍然属于默认复合层(根层叠上下文),共用同一个绘图上下文对象(GraphicsContext)。
  • 复合图层,又称图形层。它会单独分配系统资源,每个复合图层都有一个独立的GraphicsContext。(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)
  • 某些特殊的渲染层会被提升为复合成层(Compositing Layers),复合图层拥有单独的 GraphicsLayer,而其他不是复合图层的渲染层,则跟随第一个拥有 GraphicsLayer 的父层。
  • 每个GraphicsLayer 都有一个 GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,作为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,然后显示到屏幕上。
  • 复合图层的作用:
  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快(合成的处理需要用到GPU,生成合成层的位图处理是需要CPU)
  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层 3.对于 transform 和 opacity 效果,不会触发 layout 、layer和 paint,直接进入合成线程处理
  • CPU 和 GPU 之间的并行性,可以同时运行以创建高效的图形管道。
  • 复合图层创建标准:
  • 3D转换:translate3d,translateZ依此类推;
  • ,和元件;
  • transform和opacity经由Element.animate();
  • transform和opacity经由СSS过渡和动画;
  • 有合成层后代同时本身 fixed 定位;
  • will-change;拥有加速 CSS 过滤器的元素filter;
  • 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
  • 硬件加速时请使用index,使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染;具体的原理时这样的: webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低, 那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的), 会默认变为复合层渲染,如果处理不当会极大的影响性能简单点理解,其实可以认为是一个隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意
9. 图层绘制(Paint)

在完成图层树的构建之后,渲染进程会对图层树中的每个图层进行绘制,渲染进程接会把一个图层的绘制拆分成很多小的绘制指令,再把这些指令按照顺序组成一个待绘制列表,绘制列表只是用来记录绘制顺序和绘制指令的列表

10. 栅格化(raster)操作

绘制操作是由渲染进程中的合成线程来完成的,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。

  • 一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口,有的图层很大,比如有的页面使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分;
  • 所以合成线程会将图层划分为图块(tile),合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的栅格化,是指将图块转换为位图。图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的;
  • 栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中;
11. 合成和显示(draw quad)

图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到显存(后缓存区)中,最后再将显存显示在屏幕上。

12. 渲染完毕后就是load事件了,之后就是JS逻辑处理了

补充:

三个和渲染流水线相关的概念——“重排”“重绘”和“合成”。

1.更新了元素的几何属性(重排)
2.更新元素的绘制属性(重绘)
3.直接合成 更改一个既不要布局也不要绘制的属性,渲染进程将跳过布局和绘制,只执行后续的合成操作,这个过程叫做合成

上面提到,渲染完毕后会触发load事件,那么load事件与DOMContentLoaded事件的先后顺序是?

浏览器内核中线程之间的关系

GUI渲染线程与JS引擎线程互斥

由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起, GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。

从上述的互斥关系,可以推导出,JS如果执行时间过长就会阻塞页面。

譬如,假设JS引擎正在进行巨量的计算,此时就算GUI有更新,也会被保存到队列中,等待JS引擎空闲后执行。 然后,由于巨量计算,所以JS引擎很可能很久很久后才能空闲,自然会感觉到卡顿。

所以,要尽量避免JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

WebWorker(多线程?no)

前文中有提到JS引擎是单线程的,而且JS执行时间过长会阻塞页面,那么JS就真的对cpu密集型计算无能为力么?

所以,后来HTML5中支持了Web Worker

MDN的官方解释是:

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面

一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件

这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window

因此,使用 window快捷方式获取当前全局的范围 (而不是self) 在一个 Worker 内将返回错误 复制代码

因此:

  • 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
  • JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

所以,如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程, 只待计算出结果后,将结果通信给主线程即可,JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。

WebWorker与SharedWorker

既然都到了这里,就再提一下SharedWorker(避免后续将这两个概念搞混)

  • WebWorker只属于某个页面,不会和其他页面的渲染进程(浏览器内核进程)共享
  • 所以Chrome在渲染进程中(每一个Tab页就是一个渲染进程)创建一个新的线程来运行Worker中的JavaScript程序。
  • SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用
  • 所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。

看到这里,应该就很容易明白了,本质上就是进程和线程的区别。SharedWorker由独立的进程管理,WebWorker只是属于渲染进程下的一个线程。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值