从底层理解浏览器运行机制——好文解读

1,372 阅读15分钟

浏览器运行机制概览

原文 & 好文推荐,本篇文章是自己在阅读这篇推荐文章时的一些感悟与理解,这篇文章从浏览器的进程与线程角度对一些概念与浏览器的逻辑进行了比较宏观角度上的概览,我阅读时受益匪浅,建议先读原文,然后如果有不懂的看一下我的一些理解或许对你有点帮助。

1、浏览器是多进程的

浏览器是多进程的,操作系统会给每个tab标签页分配资源(cpu、内存等),但是也并不是说严格的一个标签页对应一个进程,实际的进程数量是要多与标签页数量的,因为会有主进程等相较于标签页进程来说更高层次的一些浏览器进程,而且打开一个新的标签页,浏览器进程的数量会增加不止一个,下面mac系统中监视chrome浏览器,在我只打开了4个标签页的基础上就会发现若干个进程,并且随着新标签页的打开,进程数量会增加(不止一个)

浏览器进程概览.png

当然可以直接在(chrome浏览器——>更多工具——>任务管理器)中查看更详细的浏览器相关的进程信息,这里看的更直观,会发现浏览器主进程以及标签页、浏览器插件都独立创建了相关进程,如下图:

chrome任务管理器.png

2、浏览器包含的进程

参考原文

  • Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有

    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
    • 网络资源的管理,下载等
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU进程:最多一个,用于3D绘制等
  • 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为

    • 页面渲染,脚本执行,事件处理等

自己的理解

  1. Browser主进程只有一个,相当于一个控制中心,功能是总领性质的,比如打开新的标签页时创建对应的renderer进程、将renderer进程(渲染)得到的内存中的bitmap绘制到用户界面上等
  2. 使用插件也会创建相关的插件进程
  3. 进行3D绘制的GPU进程(最多只存在一个)
  4. renderer进程,浏览器渲染进程,也就是浏览器内核做的事情,页面渲染、js的代码执行、事件处理等,当然这个进程是多线程的,不同的任务由不同的线程去做

3、浏览器多进程优势

  1. 首先所谓的浏览器多核模型并不是指使用多个真正的物理核心或处理器,而是利用操作系统提供的线程或进程来实现并行处理,多核模型的最直观的优势其实也就是操作系统多进程多线程带来的优势:增加浏览器多中行为的并发性,提高cpu运行效率
  2. 增加浏览器的稳定性和安全性,也就是说每个任务都在独立的线程或者进程中执行,如果一个任务崩溃,不会导致整个浏览器崩溃,说白了还是操作系统进程与线程带来的优势,因为进程是操作系统资源分配的基本单位,线程是资源调度的基本单位,因为它们是基本单位,所以不同进程之间,不同线程之间就不会互相影响。
  3. 浏览器插件也是独占进程的,这里有个沙盒的概念:沙盒模型(Sandbox Model)是一种计算机安全概念,用于隔离和限制程序的执行环境,以保护系统的安全和稳定性。所以多进程还有一个优势就是所谓的使用沙盒模型进行插件隔离提高稳定性。和上面还是一个意思,都是指防止一个进程崩了整个浏览器也崩。

4、浏览器内核——渲染进程

浏览器的渲染进程是多线程的,一些常驻线程:

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引擎执行。

我的理解:宏观上对各个线程的作用进行理解就是,GUI渲染线程没啥难理解的,就是处理html和css并渲染的功能;js引擎线程,也就是常说的js内核,作用也很直观,就是运行js代码;事件触发线程负责在事件队列中添加各种事件回调,比如setTimeout计时到了之后就向事件队列中添加上其回调函数;定时器触发线程就是负责计时的,说白了就是setTimeout和setIterval的计时,因为js引擎线程如果负责计时的话,如果阻塞就会影响计时的准确性,所以有必要新开一个单独负责计时的计时器触发线程;http网络请求也是由单独的请求线程负责的。

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

如果自己打开任务管理器,然后打开一个浏览器,就可以看到:任务管理器中出现了两个进程(一个是主控进程,一个则是打开Tab页的渲染进程) , 然后在这前提下,看下整个的过程:(简化了很多)

  • Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程

  • Renderer进程的Renderer接口收到消息,简单解释后,交给渲染线程,然后开始渲染

    • 渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
    • 当然可能会有JS线程操作DOM(这样可能会造成回流并重绘)
    • 最后Render进程将结果传递给Browser进程
  • Browser进程接收到结果并将结果绘制出来

自己的理解:首先明确整体架构,Browser进程就是浏览器主进程,renderer进程是浏览器渲染进程,也就是常说的浏览器内核,其中这个进程之中又有我们熟知的各个线程,比如js引擎线程、GUI渲染线程、事件触发线程、计时线程以及网络请求线程,现在我们考虑的是浏览器主进程和渲染进程这两个进程之间的通信,首先是主进程接收到用户的网页请求(网页操作,比如打开新页面)之后,通过某些接口将渲染任务传递给渲染进程,渲染进程各个线程开始协同工作,最终把渲染的结果传递给主进程,最终主进程将页面显示出来。

6、WebWorker,JS的多线程?

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

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

MDN的官方解释是:

Web WorkerWeb内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面
​
一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件 
​
这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window
​
因此,使用 window快捷方式获取当前全局的范围 (而不是self) 在一个 Worker 内将返回错误

这样理解下:

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

所以,如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程, 只待计算出结果后,将结果通信给主线程即可,perfect!

而且注意下,JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。

我的理解:我们可以在当前的js引擎线程上通过调用api的方式创建一个新的js执行线程,也就是所谓的web worker多线程,这个线程的js执行与原本的js引擎是不同的全局上下文,所以不会干扰原本的js引擎线程,这个新开的线程最后把计算所得的结果传递给js引擎线程即完成了任务,也就是所谓的js多线程,但js引擎线程是单独的一个线程这个本质问题是没有改变的。

7、WebWorker与SharedWorker

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

  • WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享

    • 所以Chrome在Render进程中(每一个Tab页就是一个render进程)创建一个新的线程来运行Worker中的JavaScript程序。
  • SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用

    • 所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。

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

我的理解:原文已经说的非常清楚了,我也是第一次知道SharedWorker这个概念,看来也是浏览器提供的一种可手动控制的提高并发的手段,只是说它是一个进程,是所有浏览器的渲染线程共享的,而WebWorker是属于某个renderer进程的一个线程。

8、浏览器渲染流程

这里只是说浏览器内核(js引擎)拿到了html文件之后如何处理:

  1. 解析html建立dom树
  2. 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
  3. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  4. 绘制render树(paint),绘制页面像素信息
  5. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。

9、load事件与DOMContentLoaded事件的先后

我的理解:说白了就是解析html生成dom完毕后,不管css等其他资源的加载完成与否就触发DOMContentLoaded事件;面上所有的DOM,样式表,脚本,图片都已经加载完成了才触发onLoad事件。顺序是:DOMContentLoaded -> load

10、css加载相关

css是由单独的下载线程异步下载的,所以css加载不会阻塞DOM树解析(异步加载时DOM照常构建),但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)

11、css硬件加速

原文参考

12、浏览器中js的运行机制

我的理解:以前只是知道宏任务与微任务影响下的js的执行逻辑,这里我结合js渲染进程的各个线程的配合关系来重新叙述一遍js事件循环——首先js引擎线程执行script中的所有同步代码(script属于宏任务),js引擎执行的代码在一个执行栈中,执行栈中的所有同步代码,对于异步代码又分为宏任务和微任务,在执行过程中如果遇到以Promise.then为代表的微任务就把微任务放到微任务队列中,这个微任务队列应该是属于js引擎线程管理,在同步代码的执行过程中如果遇到以setTimeout为代表的宏任务,就把setTimeout交由定时器触发线程,当时间条件满足时事件触发线程就会把异步任务的回调函数放入一个事件队列中,当前js执行栈中所有同步代码执行完毕后,紧接着执行微任务队列中的代码,微任务队列全部处理完毕后,当前的宏任务就算执行完毕了(js引擎线程控制的执行栈与微任务队列都空了),这时候js引擎线程应该是被挂起的,GUI线程接管渲染操作,渲染完成后继续由js引擎线程接管,js引擎线程检查宏任务事件队列,将队头取出放入执行栈中执行,相当于开启了一个新的宏任务执行,以此循环。

13、说说定时器

明白了上面的js运行机制,就可以解释定时器为什么有误差了,因为setTimeout的计时虽然是由定时器触发线程掌管的,这个计时是基本准确的,但是计时完成后并不是说立即执行回调,而是js事件触发线程将回调函数放入宏任务队列中等待,只有js引擎线程将当前的宏任务执行完毕后才会从宏任务队列中取出等待的定时器回调,所以造成了误差。

由于这种计时机制,setInterval方法会出现比较极端的情况:每隔一段时间都准时向宏任务队列中放入回调函数,但是js引擎线程并不一定此时是空闲的,所以可能照成setInterval回调函数在宏任务队列中堆积,然后突然js引擎线程空闲了,然后连续执行很多个setInterval的逻辑,造成时间偏差过大。

使用setTimeout模拟setInterval

function runTimer() {
    (function inner() {
        let t = setTimeout(() => {
            console.log('要执行的代码逻辑')
            clearTimeout(t);
            inner();
        }, 1000);
    })();
}

这样写重写setInterval也不是说就能保证准确的执行时间间隔的精度,效果就是只有本次代码逻辑执行完毕后才会开启定时器触发线程的计时,而setInterval是不停的进行计时,并把回调放入宏任务队列,而不管回调的执行情况。