浏览器层面优化前端性能(1):Chrom组件与进程/线程模型分析

科技资讯 投稿 28400 0 评论

浏览器层面优化前端性能(1):Chrom组件与进程/线程模型分析

现代操作系统已经非常健壮了,它让应用程序在各自的进程中运行和不会影响到其他程序。一个进程崩溃不会损害到其他进程以及操作系统。同时系统会严格的限制一个用户访问另外一个用户空间的数据。

浏览器属于一个应用程序,而应用程序的一次执行,可以理解为计算机启动了一个进程,进程启动后,CPU会给该进程分配相应的内存空间,当我们的进程得到了内存之后,就可以使用线程进行资源调度,进而完成我们应用程序的功能。

以chrome为例,使用IPC通信的多进程应用程序

浏览器组件

浏览器大体上由以下几个组件组成,各个浏览器可能有一点不同。

  • 浏览器引擎 – 查询与操作渲染引擎的接口

  • 网络 – 用于网络请求, 如HTTP请求。它包括平台无关的接口和各平台独立的实现

  • JS解释器 - 用于解析执行JavaScript代码

Chrome的并发模型

chrome的进程,chrome没有采用一般应用程序的单进程多线程的模型,而是采用了多进程的模型,按照他的文字说明,主界面框架下的一个TAB就对应这个一个进程。但实际上,一个进程不仅仅包含一个页面,实际上同类的页面在共用一个进程。

  • Process-per-site-instance:就是你打开一个网站,然后从这个网站链开的一系列网站都属于一个进程。这是Chrome的默认模式。

  • Process-per-site:同域名范畴的网站放在一个进程,比如www.google.com和www.google.com/bookmarks就属于一个域名内(google有自己的判定机制),不论有没有互相打开的关系,都算作是一个进程中。用命令行–process-per-site开启。

  • Process-per-tab:这个简单,一个tab一个process,不论各个tab的站点有无联系,就和宣传的那样。用–process-per-tab开启。

  • Single Process:这个很熟悉了吧,即传统浏览器的模式:没有多进程只有多线程,用–single-process开启。

多进程有好处:

多线程模型

  • Browser进程只有一个,主控整个系统的运行,管理Chrome大部分的日常事务;

    • UI thread:

      • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上

    • storage thread: 控制文件等的访问;

  • Renderer 浏览器渲染进程(浏览器内核),主要负责页面的渲染和显示:页面渲染,脚本执行,事件处理等

  • Utility Network:网络进程,负责页面网络资源的加载

  • NPAPI或PPAPI插件进程,每种类型的插件对应一个进程,仅当使用该插件时才创建。

  • Pepper插件进程

Browser作为主进程最先启动,Browser包含一个主线程mainthread,在mainthread中对整个系统进行初始化,并启动为另外几个线程,看下面的代码:

  process->db_thread(; //负责数据库处理

  process->process_launcher_thread(;

  process->io_thread(; //负责管理进程间通信和所有I/O行为。

io_thread不仅负责Browser进程的I/O,而且其他Renderer的I/O请求也会通过进程间通信发送到这个线程,由该线程进行处理,最后把结果在返回给各个Renderer进程。各个线程的功能不一样,但设计模式是一样的

对于Renderer进程,它们通常有两个线程:一个是Main thread,负责与主线程联系。另一个是Render thread,它们负责页面的渲染和交互

 

Chrome的线程模型

根据线程处理事务类别的不同,所起的消息循环有所不同。比如

  • 启用的是MessagePumpForIO类,处理UI的线程用的是MessagePumpForUI类,

不同的消息循环类,主要差异有两个,一是消息循环中需要处理什么样的消息和任务,第二个是循环流程(比如是死循环还是阻塞在某信号量上…)。

浏览器通常由以下常驻线程组成:

  • GUI 渲染线程
    GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint或由于某种操作引发回流(reflow时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被冻结了。

    • 多个工作线程(work thread)

    • 多个光栅化线程(raster thread)

  • JavaScript引擎线程
    JS为处理页面中用户的交互,以及操作DOM树、CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果JS是多线程的方式来操作这些UI DOM,则可能出现UI操作的冲突;如果JS是多线程的话,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果,当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性(多线程的话会使浏览器的效率降低。多线程必然会引入的锁,信号量的一类操作,大大增加了复杂性,JS在最初就选择了单线程执行
    GUI渲染线程与JS引擎线程互斥的,是由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致。当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
    由于GUI渲染线程与JS执行线程是互斥的关系,当浏览器在执行JS程序的时候,GUI渲染线程会被保存在一个队列中,直到JS程序执行完成,才会接着执行。因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

  • 定时触发器线程
    浏览器定时计数器并不是由JS引擎计数的, 因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。

  • 事件触发线程
    当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

  • 异步http请求线程
    在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理。

可以这样理解,页面的渲染,JS的执行,事件的循环,都在这个进程内进行。

Browser Process进程:

  • UI thread:控制浏览器上的按钮及输入框;

  • storage thread: 控制文件等的访问;

网页加载过程-导航过程

  • network thread:处理网络请求,从网上获取数据(Chrome72以后,已将network thread单独摘成network service process,当然也可以通过 chrome://flags/#network-service-in-process修改配置,将其其作为线程运行在Browser Process中

处理过程解析

处理输入

UI thread会判断输入的内容是搜索关键词(search query)还是URL,如果是搜索关键词,跳转至默认搜索引擎对应都搜索URL,如果输入的内容是URL,则开始请求URL。

开始导航

UI thread将关键词搜索对应的URL或输入的URL交给网络线程Network thread,此时UI线程使Tab前的图标展示为加载中状态,然后网络进程进行一系列诸如DNS寻址,建立TLS连接等操作进行资源请求,如果收到服务器的301重定向响应,它就会告知UI线程进行重定向然后它会再次发起一个新的网络请求。

读取响应

network thread接收到服务器的响应后,开始解析HTTP响应报文,然后根据响应头中的Content-Type字段来确定响应主体的媒体类型(MIME Type),如果媒体类型是一个HTML文件,则将响应数据交给渲染进程(renderer process)来进行下一步的工作,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。

浏览器会进行 Safe Browsing 安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。除此之外,网络线程还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。

查找渲染进程

network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。

浏览器已经预先查找和启动了一个渲染进程,如果中间步骤一切顺利,当 network thread 接收到数据时,渲染进程已经准备好了,但是如果遇到重定向,这个准备好的渲染进程也许就不可用了,这个时候会重新启动一个渲染进程。

提交导航

这个时候导航栏会更新,安全指示符更新(地址前面的小锁),访问历史列表(history tab)更新,即可以通过前进后退来切换该页面。

初始化加载完成

渲染进程

    1. GUI渲染线程

  • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow时,该线程就会执行

  • GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

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

    • 普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中,哪怕是absolute布局(fixed也一样),即使脱离普通文档流,但它仍然属于默认复合层)

    • 可以通过硬件加速的方式—GPU线程,声明一个新的复合图层(最常用的方式:translate3d、translateZ,它会单独分配资源,会脱离普通文档流,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘。

      • 如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意

    • css加载不会阻塞DOM树解析(异步加载时DOM照常构建——css是由单独的下载线程异步下载的)

因为加载css的时候,可能会修改下面DOM节点的样式,如果css加载不阻塞render树渲染的话,那么当css加载完之后,render树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。所以干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后,在根据最终的样式来渲染render树,这种做法性能方面确实会比较好一点。

  • 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)

  • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序

  • GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

    • 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)

    • JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

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

  • 事件触发线程

    • 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中。

    • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

  • 定时触发器线程

    • setInterval与setTimeout所在线程

    • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)

  • 异步http请求线程

    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

    事件循环机制进与线程关系

    JavaScript事件队列等原因还是JavaScript线程与 定时触发器线程、事件触发线程、异步http请求线程等IO通信问题。《》

    • 栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)

    如此循环,如下图

    等待栈中的代码执行完毕后才会去读取事件队列中的事件

    在ECMAScript中,microtask称为jobs,macrotask可称为task

    • macrotask中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护 

      每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行),(`task->渲染->task->...`)

      • 每一个task会从头到尾将这个任务执行完毕,不会执行其它

    • microtask(又称为微任务),microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护

      是在当前 task 执行结束后立即执行的任务

      • 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染

    分别很么样的场景会形成macrotask和microtask呢?

    • macrotask:主代码块,setTimeout、postMessage、setImmediat、MessageChannel(优先级高于setTimeout等(可以看到,事件队列中的每一个事件都是一个macrotask)、requestAnimationFrame 、I/O、UI Rendering

    • 注意promise的polyfill与官方版本的区别:

      • polyfill,一般都是通过setTimeout模拟的,所以是macrotask形式

    定时器

    上述事件循环机制的核心是:JS引擎线程和事件触发线程

    是JS引擎检测的么?当然不是了。它是由定时器线程控制(因为JS引擎自己都忙不过来,根本无暇分身)

    什么时候会用到定时器线程?当使用setTimeout或setInterval时,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中

    setTimeout与setInterval

    • 延缓事件为:setTimeout触发是设置的等待事件+等待到任务执行时间)

    而且setInterval有一些比较致命的问题就是:累计效应

    JS引擎会对setInterval进行优化,如果当前事件队列中有setInterval的回调,不会重复添加。但是,有错过了延迟的事件。

    用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame。

    Node.js事件循环与线程

    Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器(和浏览器中的是完全不相同的东西,关键还是线程架构不同)

    • V8引擎解析JavaScript脚本

    • libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎

    Node.js 的运行机制

    • V8 引擎解析 JavaScript 脚本。

    • libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。

    libuv 引擎中的事件循环6个阶段

     

      • timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调

        • 同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。

      • idle, prepare 阶段:仅 node 内部使用

      • poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情

        • 执行 I/O 回调

      • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

        • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调

    • check 阶段:执行 setImmediate( 的回调

      • setImmediate 设计在 poll 阶段完成时执行,即 check 阶段;

    • close callbacks 阶段:执行 socket 的 close 事件回调

    process.nextTick 这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

    每个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。

    function f ( {
      console.log('start'
      setTimeout(( => {
        console.log('timer1'
        Promise.resolve(.then(function( {
          console.log('promise1'
        }
      }, 0
      setTimeout(( => {
        console.log('timer2'
        Promise.resolve(.then(function( {
          console.log('promise2'
        }
      }, 0
      Promise.resolve(.then(function( {
        console.log('promise3'
      }
    
      console.log('end'
    
    }
    f(

    现在,浏览器和node12,输出顺序是一样的。推荐阅读软老师的《Node 定时器详解》

    从文章的 浏览器通常由以下常驻线程组成 里面的 渲染进程  已知,GUI渲染线程与JS引擎线程是互斥的,他们会阻塞页面渲染。所以我们从浏览器的去分析下,怎么优化前端的性能呢?

     

    前端都该懂的浏览器工作原理,你懂了吗? https://segmentfault.com/a/1190000022633988

    Chrome源码剖析、上--多线程模型、进程通信、进程模型https://www.cnblogs.com/v-July-v/archive/2011/04/02/2036008.html

    http://dev.chromium.org/developers/design-documents/multi-process-architecture

    浅析浏览器渲染原理 https://segmentfault.com/a/1190000012960187

    浏览器与Node的事件循环(Event Loop有何区别? https://blog.csdn.net/Fundebug/article/details/86487117

    转载本站文章《浏览器层面优化前端性能(1:Chrom组件与进程/线程模型分析》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/browser/webkit/2020_0610_8455.html

    编程笔记 » 浏览器层面优化前端性能(1):Chrom组件与进程/线程模型分析

    赞同 (131) or 分享 (0)
    游客 发表我的评论   换个身份
    取消评论

    表情
    (0)个小伙伴在吐槽