1. 浏览器的进程模型
浏览器是一个多进程多线程的应用程序
为了避免相互影响,每个进程都有自己的内存空间,减少连环崩溃的几率。
可以在浏览器的任务管理器中查看当前的所有进程,谷歌浏览器快捷键:Shift+Esc
2. 浏览器有哪些进程和线程?
其中,最主要的进程有:
-
浏览器主进程(Browser Process)
- 主要负责tab页的管理
- 控制浏览器的主窗口和各个子进程的创建和销毁
- 管理用户界面、存储缓存和历史记录等功能
-
渲染进程(Renderer Process)--
重点- tab页都会有一个独立渲染进程(防止一个网页的崩溃引起整个浏览器的崩溃)
- 负责界面渲染工作,解析HTML、CSS、图片渲染
- JavaScript 解析和执行:解析和执行页面中的 JavaScript 代码,处理事件响应、动态内容生成等交互操作。
-
GPU进程(GPU Process)
- 处理网页中的图像和视频,使用浏览器的硬件加速技术实现的
- 处理复杂的三维图形场景(3D)、CSS3等等视觉效果
-
网络进程(Renderer Process)
- 网络进程的工作是实现浏览器与服务器之间的网络通信。
- 负责发送和接收网络请求和响应,解析 URL,处理安全性检查,管理 Cookie 和缓存,处理跨域请求,以及进行数据传输和流控制
-
插件进程(Plug-in Process)
- 插件运行在插件进程,每个不同的插件都会运行在一个新的进程
将来该默认模式可能会有所改变,有兴趣的同学可参见chrome官方说明文档
3. 渲染进程
主要线程如下:
-
主线程(Main Thread)
- 渲染主线程是单线程
- 处理用户交互、解析 HTML、构建 DOM 树、进行布局和绘制等任务。
-
渲染线程(Renderer Thread)
- 渲染线程负责将 HTML、CSS 和 JavaScript
转换为可视化的页面。 - 负责布局(Layout)、绘制(Painting)和合成(Compositing)等任务,渲染线程是渲染进程中的辅助线程,负责执行部分与渲染相关的任务
- 渲染线程负责将 HTML、CSS 和 JavaScript
-
JavaScript 引擎线程(JavaScript Engine Thread)
解析和执行 JavaScript代码。- 常见的 JavaScript 引擎有 V8(用于 Chrome)、SpiderMonkey(用于 Firefox)和 JavaScriptCore(用于 Safari)等。
-
事件线程(Event Thread)
- 处理用户输入、网络请求、定时器等异步事件。将接收的
事件分发给适当的线程进行处理。
- 处理用户输入、网络请求、定时器等异步事件。将接收的
-
定时器线程(Timer Thread)
- 定时器线程负责管理 JavaScript 中的定时器,当定时器到期时,定时器线程会将相应的回调函数添加到事件队列中,等待事件线程执行。
- 就是
给定时任务计时,例如5秒针后执行exec()方法,当5秒针结束后,将exec()方法放入事件队列中。
-
异步 HTTP 请求线程(Asynchronous HTTP Request Thread)
- 如果页面中存在异步的 HTTP 请求(例如 AJAX 请求),则这些请求通常在单独的线程中执行,以防止阻塞主线程。
- 异步 HTTP 请求线程和浏览器的网络进程是协同工作的,当渲染进程需要发送异步的 HTTP 请求时,它会通过与网络进程之间的通信机制,将请求的详细信息传递给网络进程。
网络进程接收到请求后,会执行实际的网络操作,包括建立连接、发送请求、接收响应等。一旦响应返回,网络进程将响应数据传递给渲染进程的异步 HTTP 请求线程,以触发相应的回调函数。
3.1 渲染主线程
渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画 60 次
- 执行全局 JS 代码
- 执行事件处理函数
- 执行计时器的回调函数
- ......
渲染主线程为
单线程
原因:
确保页面的一致性,浏览器渲染主线程负责处理网页的渲染和用户交互,包括 DOM 操作、样式计算、布局和绘制等。
如果允许多个线程
同时修改页面的状态,可能会导致页面的不一致性,例如不同线程对同一元素进行不同的操作,可能会导致渲染冲突和显示错误。
3.1.1 调度任务一般原则
任务调度是由浏览器的渲染引擎负责的。任务调度的目标是确保页面的渲染和用户交互的响应性,并尽可能平衡各种任务的执行。
渲染主线程中任务调度的一般原则:
-
任务队列(Task Queue)
- 渲染主线程会维护一个任务队列,
用于存储各类任务。 - 任务队列中的任务可以是浏览器事件(如点击事件、滚动事件)、网络请求的回调、定时器任务等。
- 每个任务都有一个优先级,用于确定任务的执行顺序。
- 队列分类很多个,用来存储不同的任务类型。
- 渲染主线程会维护一个任务队列,
-
事件循环(Event Loop)
- 渲染主线程通过事件循环机制来
处理任务队列中的任务。 - 事件循环不断地从任务队列中取出任务并执行,直到任务队列为空。
- 渲染主线程通过事件循环机制来
-
响应优先级
- 渲染主线程会
根据任务的优先级来调度任务的执行顺序。 - 通常,用户交互相关的任务(如点击事件)具有较高的优先级,以保证用户操作的响应性。
- 而低优先级的任务(如定时器任务)可能会被延迟执行,以避免阻塞高优先级任务的执行。
- 渲染主线程会
-
分片和时间片
- 为了保证页面的渲染和用户交互的流畅性,
渲染主线程会将长时间运行的任务分割成较小的片段,并在执行每个片段后,让出执行权给其他任务。 - 这样可以避免长时间运行的任务阻塞渲染主线程,保持页面的响应性。
- 为了保证页面的渲染和用户交互的流畅性,
-
帧率控制
- 渲染主线程还会根据显示设备的刷新率(
通常为 60 帧/秒)来控制任务的执行。 - 它会
尽量在每个刷新周期内完成渲染和绘制操作,以保证页面的流畅性。 - 如果某个任务无法在一个刷新周期内完成,渲染主线程会将其延迟到下一个刷新周期。
- 渲染主线程还会根据显示设备的刷新率(
3.1.2 调度任务流程
- 在最开始的时候,渲染主线程会进入
一个无限循环(事件循环) - 每一次循环会
检查消息所有队列中是否有任务存在。如果有,就取出任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。 - 其他所有线程(包括其他进程的线程)可以随时
向消息队列添加任务。新任务会加到对应消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务
整个过程,被称之为事件循环(消息循环)
3.1.2 异步任务调度
代码在执行过程中,会遇到一些无法立即处理的任务,比如:
- 计时完成后需要执行的任务 ——
setTimeout、setInterval - 网络通信完成后需要执行的任务 --
XHR、Fetch - 用户操作后需要执行的任务 --
addEventListener
为了保证渲染主线程不阻塞!使用了定时器线程处理异步任务
- 当遇到异步任务时,浏览器不会
立即执行它,将其放入计时线程中。 - 计时结束后将它
放入任务队列(Task Queue)的最末尾。 - 浏览器会不断地进行事件循环,从任务队列中
取出一个任务并执行。这个过程会重复进行,直到任务队列为空。
3.1.3 任务优先级
在浏览器中,任务可以分为以下几种类型,按照执行优先级从高到低排列:
-
宏任务(Macro Task):
- 宏任务包括整体代码块(例如整个 script 标签中的代码)、setTimeout/setInterval(延时队列)、I/O 操作、UI 渲染等。
- 宏任务的执行优先级较低,会在微任务执行完毕后执行。
-
微任务(Micro Task):
- 微任务包括Promise回调、MutationObserver回调等。
- 微任务具有高优先级,会在宏任务执行完毕、页面渲染之前执行。
-
动画帧(Animation Frame):
- 动画帧任务用于执行与页面动画相关的操作,例如使用 requestAnimationFrame 方法注册的回调函数。
- 动画帧任务的优先级介于宏任务和微任务之间。
-
交互任务(User Interaction):
- 交互任务用于处理用户交互事件,例如点击、滚动等。
- 交互任务的优先级通常高于宏任务和微任务,但低于动画帧任务。
-
渲染任务(Rendering):
- 渲染任务用于执行页面渲染相关的操作,例如绘制图形、计算布局等。
- 渲染任务的优先级通常较低,会在其他任务执行完毕后进行。
执行优先级总结(从高到低):
- 微任务(Micro Task)
- 动画帧(Animation Frame)
- 交互任务(User Interaction)
- 宏任务(Macro Task)
- 渲染任务(Rendering)
注意:
- 不同浏览器可能对任务类型和执行优先级有所调整
- 事件循环取宏任务和渲染任务的队列有帧率控制,渲染时每16.6毫秒为一帧,在这一帧的中间可以穿插部分宏任务,避免宏任务队列过多导致不执行渲染任务。
事件循环的宏任务和渲染时机逻辑
for(;;){
取出宏任务;
执行宏任务
if(渲染时机是否到达){
渲染
}
}
在目前 chrome 的实现中,至少包含了下面的队列:
- 延时队列:用于存放计时器到达后的回调任务,优先级「中」
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
- 微队列:用户存放需要最快执行的任务,优先级「最高」
添加任务到微队列的主要方式主要是使用
Promise、MutationObserver例如:
// 立即把一个函数添加到微队列 Promise.resolve().then(函数)
4. 页面渲染
4.1 下载流程
由网络进程下载HTML文件,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。
在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。
4.2 渲染流程
- 解析 HTML
- 当用户访问一个网页时,浏览器会
下载对应的 HTML文件。 - 将HTML文件
解析为 DOM(文档对象模型)树的结构,DOM 树表示了 HTML 文档的结构和内容。
- 当用户访问一个网页时,浏览器会
- 构建 CSSOM
- 同时,浏览器也会下载对应的 CSS 文件,并开始解析。
- 将 CSS
解析成 CSSOM(CSS 对象模型),CSSOM 表示了页面中所有样式规则的层级结构和继承关系。
- 创建渲染树
- 浏览器将
DOM 树和 CSSOM 结合起来,创建渲染树(Render Tree)。 - 渲染树只包含需要显示的元素节点(例如 div、p 等),并且考虑了每个元素节点的样式信息。
- 浏览器将
- 布局(Layout)
- 布局过程确定了每个渲染树节点在屏幕上的位置和大小。
- 浏览器会
计算每个元素的盒模型(Box Model),包括宽度、高度、边距、填充等属性。
- 绘制(Painting)
- 浏览器使用布局计算出来的信息,
将渲染树节点绘制到屏幕上。 - 绘制过程将渲染树节点转换为像素,使用图形库将像素绘制到屏幕上。
- 浏览器使用布局计算出来的信息,
- 合成(Composition)
- 如果页面中存在多个图层(例如有定位、透明度等效果的元素),浏览器会
对些图层进行合成。 - 合成过程将图层按照正确的顺序进行组合,以提高渲染性能。
- 如果页面中存在多个图层(例如有定位、透明度等效果的元素),浏览器会
- 显示页面
- 浏览器将
渲染结果显示在用户的屏幕上,用户可以看到完整的页面。
- 浏览器将
- 当浏览器渲染网页时,会将
DOM 树和 CSSOM 树合并成一个 Render Tree(渲染树),然后进行布局和绘制,最终呈现在用户界面上。 - 渲染树
只包含需要显示的元素节点,并且考虑了每个元素节点的样式信息。由于渲染树中只包含需要显示的元素节点,因此它的结构可能与 DOM 树不完全一致,例如隐藏的元素和无需显示的节点可能会被从渲染树中移除。
4.2.1 DOM 树
表示 HTML 文档结构的一种树形结构,它将 HTML 文档中的每个标签以及标签之间的关系转化为一个节点树形结构。每个 HTML 标签都对应 DOM 树中的一个节点,包括文本节点、元素节点、属性节点等。DOM 树的根节点是文档节点,表示整个 HTML 文档,而子节点则对应 HTML 中的标签和属性。
查看方式
- 方式一:开发者工具,Elements(元素)选项卡就是DOM 树。
- 方式二:
document.documentElement可以获取DOM树。
4.2.2 CSSOM 树
是表示 CSS 样式信息的一种树形结构,它将 CSS 文件中的每个样式规则转化为一个节点树形结构。每个样式规则都对应 CSSOM 树中的一个节点,包括规则节点、选择器节点、样式属性节点等。CSSOM 树的根节点是样式表节点,表示整个 CSS 文件,而子节点则对应 CSS 中的选择器和样式属性。
查看方式
- 方式一:浏览器F12打开开发者工具,可以在 Elements 选项卡右侧的 Styles(样式)面板就是CSSOM 树。
- 方式二:js:
document.styleSheet可以获取CSSOM树。
4.3 解析HTML和CSS
下载HTML文件,从上往下依次解析。- 在
解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。 - 解析过程
遇到CSS引用解析CSS,遇到JS引用执行 JS。- 如果主线程解析到
link位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。 - 如果主线程解析到
script位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。
- 如果主线程解析到
当步骤完成后会得到DOM树和CSSOM树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。
注意:
css的加载是会阻塞后续js的执行的,后续js会等待css加载完成后才会执行。css的加载并不会阻塞Dom Tree的构建。css的加载是会阻塞页面渲染的,因为页面渲染的Render Tree(渲染树)是需要css om和dom tree进行合并从而渲染页面的。
4.4 生成渲染树(Render Tree)
样式计算。
样式计算是在生成 CSSOM 树过程中进行,主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。完成后,会得到一棵带有样式的 DOM 树。
比如:很多预设值会变成绝对值,比如red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成px
查看:
浏览器F12打开开发者工具,可以在 Elements 选项卡右侧的Computed面板就是计算之后的最终样式。
在样式计算完成后,将 DOM 树和 CSSOM 树进行合并,生成渲染树(Render Tree)。渲染树只包含需要显示在页面上的可见元素节点。
4.5 布局
布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。
DOM 树和布局树并非一一对应。
比如display:none的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。
4.6 分层和分块
分层(Layering)是在布局(Layout)阶段之后进行的。
在布局完成后,渲染引擎根据渲染树的层叠顺序和一些优化策略,将渲染树中的元素节点进行分层操作。分层的目的是为了在后续的绘制阶段提供更高效的渲染操作,将不同层级的元素节点进行离屏渲染或合并渲染。
分层好处
分层的好处在于,将来某一个层改变后,
仅会对该层进行后续处理,从而提升效率。列如:滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过
will-change属性更大程度的影响分层结果。查看分层
打开开发者工具后,最右侧有三个小点--->More tools--->Layers
分块(Chunking)是
在布局(Layout)后并行进行的优化步骤。它将绘制区域划分为多个块,每个块单独执行绘制操作。这样可以通过并行计算和渲染,提高渲染性能和响应速度。
4.7 绘制
在分层和分块之后,将渲染树中的元素节点
转换为图像或位图,并进行实际的视觉绘制操作。
绘制过程包括填充颜色、绘制边框、渐变、图片等,以生成最终的可视化效果。
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
合成线程首先对每个图层进行分块,将其划分为更多的小区域。
它会从线程池中拿取多个线程来完成分块工作。
分块完成后,进入光栅化阶段。 在绘制完成后,将绘制得到的图像分割为小块,生成栅格(Raster)或位图数据。这个过程通常涉及将矢量图形转换为像素图形,以便在显示设备上进行显示。
合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。
GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。
光栅化的结果,就是一块一块的位图
最后一个阶段就是画了
合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。
指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。
变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。
合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。
4.8 重新渲染
当JavaScript动态改变了页面或者CSS有动画效果时,页面重新渲染的流程如下:
- 页面的DOM(文档对象模型)或者 CSSOM树 发生了变化,比如修改了某个元素的内容、样式或属性等。
- 浏览器检测到变化,触发重绘(reflow)或重排(repaint)事件。
- 浏览器开始重新构建渲染树(Render Tree),根据新的DOM树和CSSOM树生成新的渲染树。
- 根据新的渲染树,浏览器计算出每个元素的新位置和大小,形成新的布局。
- GPU线程根据新的布局信息,绘制页面中的每个元素,完成页面的重新渲染。
需要注意的是,重绘和重排是两个不同的概念:
- 重排(reflow):指浏览器重新计算元素的位置和大小的过程,它是必需的,因为DOM的改变可能影响到其他元素的布局。重排通常会导致页面尺寸的变化。
- 重绘(repaint):指浏览器重新绘制元素的过程,包括背景色、文字颜色、边框颜色等。重绘通常不会影响页面的布局。
在现代浏览器中,为了提高渲染效率,一些优化技术被引入,例如异步渲染、分块渲染、使用requestAnimationFrame等技术。这些技术可以减少重排和重绘的次数,从而提高页面的渲染速度和性能。
4.8.1 触发reflow和repaint条件
Reflow是指浏览器根据新的CSS样式重新计算页面元素的位置和大小的过程。以下是一些会导致reflow发生的情况:
- 添加或删除样式:当添加或删除样式时,浏览器必须重新计算页面的布局和元素的位置。
- 内容的改变:当页面的内容发生改变时,比如用户在输入框中输入文字,浏览器必须重新计算页面的布局和元素的位置。
- 激活伪类:当激活如hover、active等伪类时,浏览器必须重新计算页面的布局和元素的位置。
- 操作class属性:当通过操作class属性来改变元素的样式时,浏览器必须重新计算页面的布局和元素的位置。
- 脚本操作DOM:当使用JavaScript来操作DOM时,浏览器必须重新计算页面的布局和元素的位置。
- 设置style属性:当通过设置style属性来改变元素的样式时,浏览器必须重新计算页面的布局和元素的位置。
Repaint是指浏览器根据新的CSS样式重新绘制页面元素的过程。以下是一些会导致repaint发生的情况:
- 改变窗口大小:当窗口大小改变时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
- 改变文字大小:当文字大小改变时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
- 激活伪类:当激活如hover、active等伪类时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
- 操作class属性:当通过操作class属性来改变元素的样式时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
- 脚本操作DOM:当使用JavaScript来操作DOM时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
- 设置style属性:当通过设置style属性来改变元素的样式时,浏览器必须重新计算页面的布局和元素的位置,同时也会触发repaint过程。
总之,repaint和reflow是不可避免的,只能尽可能地减少它发生,以提高页面的性能和用户体验。
为什么 transform 的效率高?
因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段
由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。