js异步——浅谈Chrome浏览器架构
前言
在讲述事件循环和消息队列之前,需要了解 JS 的单线程执行机制,JS 的执行是从上到下依次执行的,这些便是同步任务,而 ES6 引入了 Promise 对象,使得异步任务开始频频出现在 JS 的代码中。
异步任务不同于顺序执行的同步任务,他对于 JS 运行时来说是一个黑盒,无法预知他究竟什么时候会被执行,因为这取决于异步任务何时从消息队列中出队执行,而消息队列中的异步任务是否出队,则与事件循环机制直接相关。
从多进程(process)和单线程(thread)谈起
人们使用的现代浏览器都是多进程的应用程序,而运行在浏览器上的 JS 代码是单线程的。
浅谈Chrome浏览器架构
如果自己设计一个浏览器,浏览器可以是哪种架构呢?
- 单进程架构(线程间进行通信)
- 多进程架构(进程间 IPC (Inter-Process Communication)通信)
如果你的浏览器要以单进程架构进行设计,需要在一个进程内实现网络、调度、存储、IO设备、渲染、插件等任务,当然你可以把这些任务分为若干个线程去执行,形成单进程多线程的浏览器架构。
但是由于这些任务在现在操作系统中越来越复杂,例如把网络、存储、渲染这些任务放在一个线程中,执行效率和性能越来越低下(比如有一些网页的代码存在内存泄露,即便关闭这些网页线程,进程中的这块内存也无法被回收,除非关闭浏览器,否则越用越卡),且无法再向下拆分出类似线程的子空间,因为线程已经是最小的执行单位。
因此,为了强化浏览器的各个复杂功能,出现了多进程架构的浏览器,可以将网络、存储、渲染、IO、插件这些复杂任务分配给一个个单独的进程,这样每个进程又能向下拆分出多个线程,极大程度上强化了浏览器。
-
Browser 进程:Tab之外的一切都有该进程处理。负责地址栏、书签栏、前进后退、网络请求、文件访问等;
-
Renderer 进程:负责一个 Tab 内所有和网页渲染有关的事情,是最核心的进程;
-
Plugin 进程:负责 Chrome 插件相关的任务;
-
GPU 进程:GPU进程与其他浏览器进程相隔离处理GPU任务,把浏览器的页面内容绘制到屏幕上;
所有应用程序都要在OS的调度下基于CPU和GPU的计算才能运行。因为GPU要处理多个应用程序的的请求,浏览器的的GPU进程只是一个分量。GPU擅长处理图形,因此提供GPU计算的应用程序可以实现快速渲染和平滑交互。
Chrome 的每一个Tab 选项卡都拥有自己的 Renderer 进程,有三个 Tab 就意味着有三个不同的 Renderer 进程这样可以保证多个 Tab 之间互不影响,即使其中一个 Tab 没有响应,也不影响其他 Tab 的正常执行。然而,由于进程是 OS 中拥有资源的独立单位,多个 Tab 之间的数据是非共享的,这也意味着多个 Tab 都会有相同的 V8引擎初始化数据,这意味着更多的内存使用。
了解 Browser 浏览器进程
简单来说,在浏览器中,Tab之外的一切都归浏览器进程所接管,它包含3个主要的线程:
- UI thread UI线程:负责绘制和管理浏览器的按钮和输入框区域。
- Network thread 网络线程:负责处理网络堆栈以从互联网接收数据
- Storage thread 存储线程:负责控制文件访问
现在让我们来模拟一个在地址栏输入网址,并将网页呈现在浏览器上的过程
-
用户在地址栏中键入字符串,UI 线程会识别该字符串是 URL 还是搜索关键词。
Chrome中的地址既可以访问网页,同时又是个搜索框,这里假设我们输入的是 URL。
-
UI 线程通知网络线程开始进行导航,发起网络请求
-
读取响应数据,如果响应的是 HTML 文件,那么下一步会将该数据传递给渲染进程;但如果响应数据是一个压缩包或其它类型的文件,那么就意味着我们发送的是下载请求,所以需要把数据传递给下载管理器
-
UI线程负责找到渲染进程,通知它要进行网页渲染
-
此时数据和渲染进程都已经准备好,浏览器进程和渲染进程开启 IPC 传递数据,导航部分完成,你会发现tab由原网页台跳转到空白页面,然后开始边传输HTML 边进行网页渲染。
了解最为重要的 Renderer 渲染进程
渲染进程主要包括4个线程:
- Main thread 主线程:执行js、下载资源、计算样式、进行布局、绘制合成
- Raster thread 光栅线程
- Compositor thread 合成线程
- Worker thread 工作者线程
主线程的功能
-
执行 JS:主线程在遇到
<script>
标签时会阻塞HTML文档的解析,并必须先下载、解析和执行js代码,why?因为 js 可以用document.write()
之类的东西改变 DOM 结构。这就是为什么会暂停HTML的解析,并等待js代码执行完毕后才能恢复。 -
下载外部资源:如果HTML中由需要加载外部资源的标签,这在解析HTML构建DOM树之前会由预加载扫描线程检测到,并提前利用 Browser 线程的 Network 线程来下载
<img/>
、CSS和 JS的<link>
等渲染DOM需要的外部资源文件,这减少了解析 HTML 的阻塞时间 -
解析HTML:由 HTML解析器解析 HTML 内容,首先由分词器检测出各个标签名,我们称他们为token,然后利用token栈和括号匹配算法,构建出DOM树
-
计算CSS样式:主线程会基于CSS选择器或者浏览器默认样式去进行样式计算,最终在DOM树的每个对应节点上挂载其 CSS 计算样式
-
确定布局结构Layout:只有DOM节点和和它的样式可不够,还需要确定他们之间的位置关系,并构建与DOM树类似的布局树,其中包含坐标、边界框大小之类的信息
-
分层(Layer):绘制的顺序是什么呢?不同的绘制顺序会导致不用的覆盖结果,因此主线程还要根据层级关系(比如z-index、页面滚动、3d属性等)确定DOM 元素绘制的顺序,通常会生成多个图层,并构建出层树Layer Tree。
-
计算绘制列表:根据布局树和绘制顺序,计算得到绘制列表。注意是根据布局树而非层树产生绘制列表
合成器线程
合成是一种将页面的各个部分分离成层,分别光栅化它们,并在称为合成器线程的单独线程中合成为页面(合成帧)的技术。如果发生滚动,由于层已经光栅化,所以它所要做的就是合成一个新的合成帧。动画可以通过移动层和合成新的帧以同样的方式实现。
一旦创建了层树并确定了绘制列表,主线程就会将该信息提交给合成器线程。然后,合成器线程将光栅化每一层。一个层可以像一个页面的整个长度一样大,所以合成器线程将它们划分为瓦片(图块),并将每个瓦片发送到光栅线程。光栅线程对每个瓦片进行光栅化(生成像素级的位图),并将其存储在GPU内存中。
将这些信息转换为屏幕上的像素称为光栅化
- 浏览器滚动时,合成线程会创建一个新的合成帧发送给 GPU,以显示到屏幕上
- 合成线程工作与主线程无关,不用等待样式计算和 js 的执行,因此合成线程相关的动画比涉及到主线程重新计算样式和执行 js 的动画更加流畅
主线程、合成线程和光栅线程配合工作的过程
- 主线程遍历布局树从而生成层树
- 合成器线程将每个层划分为多个大小相同的图块,然后送给光栅化线程池,生成各个图块的位图,存储在GPU内存(不知道这个有什么用)
- 然后合成线程将上一步中各个图块的位合成为合成帧,并通过 IPC 传递给 Browser进程,再由Browser进程发送给GPU,从而显示在屏幕上。
整个渲染的流水线如下图所示:
参考
Inside look at modern web browser (part 1)
浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了