人法地,地法天,天法道,道法自然
大家好,我是柒八九 。一个专注于前端开发技术/Rust
及AI
应用知识分享 的Coder
。
前言
在最近的工作和学习中,有一个词总是在眼前挥之不去--EventLoop
。而在之前,其实我们讲过相关的内容,Event Loop 可视化解析
上文我们从偏JS
调用机制的角度分析了,调用栈(Call Stack
)/宏任务队列 (Task Queue
)和微任务队列 (Microtask Queue
)他们之间的关系和他们是如何协同合作的。并且,举了很多例子,用可视化的方式讲解它们如何工作的。
而今天,我们从浏览器内部的实现细节来谈谈EventLoop
是如何从接受任务到渲染出对应页面的。
也就是下图中所涉及到的各个重要节点。在阅读完本文后,希望大家能对下面有一个清晰的认知。
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 前置知识点
- 事件循环(Event Loop)
- 任务队列/微任务队列/调用栈
- 在渲染队列中执行的是什么?
- EventLoop模型
1. 前置知识点
前置知识点 ,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略
同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履 。以下知识点,请酌情使用。
页面刷新术语
我们在页面是如何生成的(宏观角度)一文中提到过这些指标,这里就拿来主义了。
- 屏幕刷新频率
- 一秒内屏幕刷新的次数(一秒内显示了多少帧的图像),单位
Hz
(赫兹),如常见的60 Hz
。刷新频率取决于硬件的固定参数(不会变的)。
- 一秒内屏幕刷新的次数(一秒内显示了多少帧的图像),单位
- 逐行扫描
- 显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。
- 以
60 Hz
刷新率的屏幕为例,这一过程即1000 / 60 ≈ 16ms
。 - 当扫描完一个屏幕后,设备需要重新回到第一行 以进入下一次的循环,此时有一段时间空隙,称为
VerticalBlanking Interval(VBI)
。
- 帧率 (Frame Rate)
- 表示 GPU 在一秒内绘制操作的帧数 ,如
60 fps
,即每秒钟GPU最多绘制 60 帧画面。 - 帧率是动态变化 的,例如当画面静止时,
GPU
是没有绘制操作的,屏幕刷新的还是buffer
中的数据,即GPU最后操作的帧数据。
- 表示 GPU 在一秒内绘制操作的帧数 ,如
- 画面撕裂(tearing)
- 一个屏幕内的数据来自2个不同的帧,画面会出现撕裂感。
测试帧率
我们可以借助requestAnimationFrame
通过每个测量前后帧
发生的时间间隔,来从侧面查看本地浏览器帧率。
javascript
const checkRequestAnimationDiff = () => {
let prev;
function call() {
requestAnimationFrame((timestamp) => {
if (prev) {
console.log(timestamp - prev);
// 应该大约是60FPS的16.6毫秒
}
prev = timestamp;
call();
});
}
call();
}
checkRequestAnimationDiff();
随意打开一个网站,并将上述代码贴到devtool-Console
运行。
下面是,我们在React-官网中实验的结果。
从输出结果来看,虽然结果不是唯一,但是它们的值都稳定在16.67~16.68
。和我们60fps
是吻合的。
WebAPI
WebAPI
工作的原理依赖于浏览器作为宿主环境
来提供和执行这些API。在Web开发中,我们通常指的WebAPI
是浏览器内置的API ,它们允许开发者利用JavaScript
与浏览器的功能进行交互。
APIs | 描述 |
---|---|
网络请求 (Network Requests) | 使用XMLHttpRequest 或fetch API,可以发起异步的HTTP请求到服务器,并在不刷新页面的情况下获取或发送数据。 |
DOM操作 (DOM Manipulation) | 浏览器提供了一套DOM API,允许JavaScript 访问和操作页面上的元素。比如,可以添加、删除或更改元素,或者修改元素的样式和内容。 |
事件处理 (Event Handling) | WebAPI允许注册事件处理程序来响应用户行为(如点击、滑动)或浏览器事件(如页面加载、窗口尺寸变化)。 |
存储机制 (Storage Mechanisms) | 浏览器提供了如localStorage 、sessionStorage 和IndexedDB 等API,可以在用户的设备上存储数据。 |
设备API (Device APIs) | 可以访问设备的硬件,如摄像头、麦克风、地理位置等,这通常通过navigator 对象暴露的API实现。 |
图形和动画 (Graphics & Animation) | Canvas 和WebGL API允许在网页上绘制二维和三维图形。requestAnimationFrame 为动画提供了一个优化的循环机制。 |
性能监控 (Performance Monitoring) | Performance API提供了获取浏览器性能相关数据的接口,帮助开发者监控和优化网页性能。 |
其他API (Other APIs) | 还有诸如Web Audio API 、WebRTC 、WebSocket 等,使得在网页上实现复杂的音频处理、实时通信成为可能。 |
WebAPI的工作流程
-
调用API :开发者在
JavaScript
代码中调用某个WebAPI
。 -
浏览器解释执行 : 浏览器解释
JavaScript
代码,并执行相应的API调用。 -
API内部处理 :
WebAPI
内部可能会执行多种操作,如触发网络请求、访问数据库、启动硬件设备等。 -
回调和事件循环 :对于异步操作,
WebAPI
通常会使用回调函数
或Promise
来处理操作完成后的结果。浏览器的事件循环
机制确保了这些回调在适当的时候被调用。 -
渲染和更新:对于涉及视觉变化的API,如DOM操作或Canvas绘图,浏览器会更新页面内容,这通常发生在浏览器的下一个重绘周期。
在整个过程中,浏览器的角色是中介,它提供了执行API的环境和必要的安全措施。这些API让Web应用可以像本地应用一样丰富和强大,同时仍然运行在浏览器这个相对安全的沙箱环境中。
下面的图,展示了WebAPI
的地位(中间部分)。
GPU硬件加速
GPU(Graphics Processing Unit)硬件加速 是一种利用GPU
来执行图形和计算任务的技术。在Web开发中,GPU硬件加速
可以通过利用用户计算机中的GPU资源来加速浏览器的渲染和绘制操作。这通常可以提高网页的性能和流畅度,尤其是对于需要大量图形操作的页面。
在Web开发中,一些CSS属性
和操作可以触发GPU硬件加速,以便更有效地利用GPU资源。
-
3D 变换(
transform
)-
使用
transform
属性进行3D变换
,如translate3d
、rotate3d
、scale3d
等,可以触发GPU硬件加速。例如:css.element { transform: translate3d(0, 0, 0); }
-
-
CSS 动画(
animation
)和过渡(transition
):-
使用CSS动画和过渡属性,例如
transform
属性的过渡,可以触发GPU硬件加速。例如:css.element { transition: transform 0.3s ease-in-out; }
-
-
Canvas 绘图:
- 在
<canvas>
元素上进行绘图操作通常会利用GPU硬件加速。这包括使用2D
或WebGL
上下文进行图形渲染。
- 在
-
使用
will-change
属性:-
will-change
属性告诉浏览器某个属性将会被改变,从而可以提前进行优化。例如:css.element { will-change: transform; }
-
-
使用
image-rendering
属性:-
image-rendering
属性用于指定图像的渲染质量,而且在某些情况下也能触发GPU硬件加速。例如:css.element { image-rendering: pixelated; }
-
-
使用
backface-visibility
属性:-
backface-visibility
属性用于指定当元素不面向屏幕时是否可见。在某些情况下,该属性的使用可以触发GPU硬件加速。例如:css.element { backface-visibility: hidden; }
-
-
使用
filter
属性(某些情况下):- 在某些情况下,使用
filter
属性(如模糊、对比度等)可能触发GPU硬件加速。
- 在某些情况下,使用
还记得我们在你会在浏览器中打断点吗?我会!中介绍过如何看chromium 在线仓库
那我们就从源码的角度来看看,为什么上面的属性会走GPU
硬件加速
,,,
或者我们可以看compositing_reason_finder.cc
这个文件,它例举了很多枚举类型。
2. 事件循环(Event Loop)
事件循环就是一个死循环,不死不休。
旧的操作系统不支持多线程,它们的事件循环可以被大致描述为一个简单的循环:
javascript
while (true) {
if (hasTasks()) {
executeTask();
}
}
现代操作系统的调度器(schedulers
)非常复杂。它们有优先级设置、执行队列等许多其他技术。
这里做一个题外话,看到schedulers/优先级设置
是不是想到React-Fiber
架构了。其实,React
在内部就是模仿操作系统,做了自己的实现逻辑。(这里就不展开说明了)
为了让事情简单化,我们可以将事件循环(Event Loop
)描述为一个循环,该循环检查是否有任何待处理的任务:
任务触发器
浏览器属于事件驱动 的技术框架,如果想让Event Loop
探查并执行对应的任务,首先要做的就是将某些任务进行触发。也就是唤起指定任务的触发器。
下面就是我们平时能够接触到的任务触发器
-
<script>
标签 :通过HTML
的<script>
标签引入的代码会被浏览器解析并执行,相关的同步任务会被放入事件循环
中。 -
延后的任务:
setTimeout
:设置一个计时器,在指定的延时后执行一段代码。setInterval
:设置一个计时器,按照指定的时间间隔重复执行一段代码。requestIdleCallback
:安排一个函数在浏览器空闲时期被调用。
-
浏览器API的事件处理程序:
- 用户触发的事件,例如
click
,mousedown
,input
,blur
等。 - 代码生成的事件,比如
XMLHttpRequest
的响应处理、fetch
API的promise resolve等。
- 用户触发的事件,例如
-
Promise状态变化 :当一个
Promise
对象的状态改变时(例如从pending
变为fulfilled
或rejected
),相关的任务会被加入事件循环
。 -
观察者:
DOMMutationObserver
:用于观察DOM
变动,当DOM
发生变化时可以通知应用。IntersectionObserver
:用于观察元素是否进入了父元素或视口的特定区域。
-
requestAnimationFrame
:用于在下一次重新渲染前执行动画或视觉更新的函数,使动画流畅。
上面的任务几乎都是通过WebAPI
进行触发的。
例如,我们在代码中有这样一行:setTimeout(function a() {}, 100)
。当我们执行setTimeout
时,WebAPI
将任务
延迟了100毫秒。100毫秒后,WebAPI
将函数a()
放入任务队列(Task Queue)
(也可以称为Callback Queue
)中。事件循环在下一个循环中获取该任务并执行它。
JS执行和页面渲染是难兄难弟
EventLoop
=TaskQueue
+RenderQueue
在之前的文章中,我们提到过文档对象模型
(DOM
)是一个应用编程接口(API
),通过创建表示文档的树,以一种独立于平台和语言的方式访问和修改一个页面的内容和结构。
在HTML文档中,Web开发者可以使用JS
来CRUD
DOM 结构,其主要的目的是动态改变HTML文档的结构。
- 读取DOM元素的数据:大小、属性、位置等
- 改变属性:data-属性、宽度、高度、位置、CSS属性等
- 创建/删除HTML节点
而在JS
把玩DOM
之后,就将其扔给了浏览器的渲染引擎,而渲染引擎任劳任怨的处理DOM
和DOM
携带的附带信息,并将其渲染成用户心仪
的页面。
也就是说JS和浏览器(渲染引擎)都能染指过DOM。
基于上面的特定的背景,我们可以得出一个结论,执行JS
和渲染页面
都在同一个线程里。
这意味着事件循环
包含渲染流程
。而渲染流程不是单一的操作。其实,它是渲染队列
:
现在EventLoop
处理链路上有两个任务源。
- JS任务 -
SomeJsTasks
- 渲染任务 -
RenderQueue
屏幕更新
对于浏览器来说,事件循环
与帧(frames
)密切相关,因为EventLoop
同时执行JS代码并渲染页面。
可以将
帧
视为屏幕状态的快照
,即用户在某一时刻看到的画面。
我们在Chromium 最新渲染引擎--RenderingNG也介绍过frames
。
浏览器的目标是尽快显示页面更新,考虑到硬件
和软件
的限制:
- 硬件限制:屏幕刷新率
- 软件限制:操作系统设置、浏览器及其设置、节能设置等
绝大多数浏览器/操作系统支持60帧每秒
(Frames Per Second
,FPS
)。浏览器试图以这个特定的速率更新屏幕。
当我们使用
60 FPS
时,这意味着浏览器在必须渲染新帧之前有16.6毫秒的时间段来执行任务 (1000/60
),而渲染新帧也会消耗时间。
3. 任务队列/微任务队列/调用栈
浏览器使用两个队列来执行我们的JS代码:
任务队列
(TaskQueue
)或宏任务队列
专用于所有事件
、延迟任务
等。微任务队列
(Microtask Queue
)用于处理promise
回调(已解决和已拒绝),以及MutationObserver
。这个队列中的单个元素被称为微任务
。
任务队列(Task Queue)
当浏览器收到一个新任务时,它将任务放入任务队列。每个事件循环
定期从任务队列
中获取任务并执行它。任务完成后,如果浏览器有时间(渲染队列
没有任务),事件循环从任务队列获取另一个任务,直到渲染队列接收到任务为止。
案例分析1
我们有3个任务:A
、B
、C
。事件循环
获取并执行第一个任务,花费了4毫秒。然后事件循环检查其他队列(微任务队列
和渲染队列
),它们是空的。事件循环
执行任务B,花费了12毫秒。总共两个任务使用了16毫秒。然后浏览器将任务添加到渲染队列
以绘制新帧。事件循环
检查渲染队列
并开始执行渲染队列中的任务,它们大约花费1毫秒。完成这些操作后,事件循环
返回到任务队列
并执行最后一个任务C。
事件循环
无法预测任务将花费多少时间。此外,事件循环无法暂停任务来渲染帧,因为浏览器引擎不知道该任务是否会对绘制内容有修改动作,还是任务只是为了渲染帧做了一些无关痛痒的准备工作。
在执行JS代码期间,JS所做的所有更改并不会直接呈现给用户,而是等到宏任务和所有待处理的微任务完成后才会表现出来 。但是,此时在JS中可以获取到最新DOM的变更信息
。
案例分析2
队列中只有2个任务(A
、B
)。第一个任务A花费了240毫秒(无法中断)。由于60FPS意味着每16.6毫秒
应该渲染一帧,所以浏览器有14帧的空窗期。当任务A结束时,事件循环执行渲染队列
中的任务以绘制新帧。
尽管我们失去了14帧,这并不意味着我们将连续渲染15帧。它只会渲染最后一帧。
这就是当JS中有长任务执行时,会阻塞页面的渲染,如果这14帧中间操作了过多的DOM
,页面中就会有一种从第一帧到第十五帧的跳动。这就是为什么我们总是要将长任务拆分成很多小任务的原因。
调用栈(Call Stack
)
在Event Loop 可视化解析讲解中我们对调用栈
有过介绍。
案例分析
javascript
function D() {
debugger;
console.log('前端柒八九');
}
function C() {
D();
}
function B() {
C();
}
function other() {
// 不在我们考察堆栈上下文中
}
function A() {
const arr = [];
while (arr.length < 2) {
arr.push(other());
}
B();
}
console.log(A());
将上面的代码贴到devTool-Console
或者devTool-Source-Snippet
中执行。这段代码将在debugger
处暂停。
这里多提一嘴,关于如何在浏览器中优雅的调试断点,可以参考你会在浏览器中打断点吗?我会!
console.log(A());
,它是调用栈的开始。- 然后我们进入
A
函数,并多次调用other
。在我们到达debugger
之前,这个函数不会出现在调用栈中,因为它在我们到达调试器之前就结束了。
这是我们在debugger
停止时调用栈的样子:
图中(anonymous
)表示全局作用域,我们没贴出来,它就是指向25行
调用栈的入口的。
当调用栈为空时,当前任务完成。
微任务
微任务只有两个可能的来源:
Promise
回调(onResolved
/onRejected
)MutationObserver
回调。
微任务有一个主要特征,使它们与其他任务完全不同:
一旦
调用栈
为空,微任务将立即执行。
微任务可以创建其他微任务,这些微任务将在调用栈结束时执行 。每个新的微任务都会推迟执行新的宏任务或新帧的渲染。
案例分析
在这个例子中,微任务队列中有4个微任务:
要执行的第一个微任务是A
。A
花费了200毫秒,而且我们在渲染队列
中有任务。然而,它们将被推迟,因为我们仍然有3个微任务。这意味着在执行A后,事件循环
将执行微任务B
、C
,最后是D
。当微任务队列变空时,事件循环渲染新帧 。在这个例子中,这4个微任务花费了0.5秒。在这段时间内,浏览器UI被阻塞,不可交互。
后续的微任务可以阻塞网站UI,使页面变得不可交互。
这个微任务特性既可能是优势也可能是劣势。例如,当 MutationObserver
根据DOM更改调用其回调时,用户在回调完成之前看不到页面上的更改。因此,我们可以有效地管理用户看到的内容。
更新后的事件循环图示:
各自的特性
调用栈
是用于跟踪正在被执行 函数的机制,而宏任务队
列是用于跟踪将要被执行函数的机制。宏任务队列
和微任务队列
都是FIFO (先进先出)的队列结构,这些任务是同步阻塞的
4. 在渲染队列中执行的是什么?
其实,在浏览器中渲染页面是有很多步骤的。
同时还涉及多个进程之间的通信。这在之前的页面是如何生成的(宏观角度)有过介绍,这里就不在罗嗦了。
而今天呢,我们从浏览器渲染帧的角度来看到底发生了啥?!其实帧渲染
不是一个单一的操作,它有几个阶段,每个阶段都可以分为子阶段。
RequestAnimationFrame(RAF)
requestAnimationFrame
是一个由浏览器提供的 JavaScript API,用于在下一次浏览器重绘之前执行指定的函数。这个函数通常用于执行动画或其他需要高性能更新的任务,因为它会在浏览器的绘制周期内运行,以确保动画的平滑流畅。
特点
-
与浏览器的重绘同步:
requestAnimationFrame
的执行时机与浏览器的重绘周期相同,通常是每秒60次(60帧每秒),这确保了动画的流畅性。 -
自动暂停: 当用户切换到其他标签页或最小化浏览器时,
requestAnimationFrame
会自动暂停,从而节省系统资源。 -
避免卡顿: 由于与浏览器的绘制同步,
requestAnimationFrame
可以避免由于连续执行任务导致的卡顿和性能问题。 -
RAF
的回调有一个DOMHighResTimeStamp
参数,它是自时间起源以来经过的毫秒数,即文档生命周期的开始。我们不需要在回调中使用performance.now()
; -
RAF
返回一个描述符(id
),因此你可以使用cancelAnimationFrame
取消RAF回调(就像使用setTimeout
一样); -
更改元素大小或读取元素属性的JS代码会强制使用
requestAnimationFrame
;
样式重新计算(Recalc Style
)
浏览器重新计算应用的样式
。此步骤还会计算哪些媒体查询
将处于活动状态。
以下操作能触发重新计算包括
- 直接更改,比如
a.styles.left = '10px'
- 通过CSS文件描述的更改,比如
element.classList.add('my-styles-class')
。
此过程可能触发整个DOM树的整体计算也可以是局部小范围的计算过程,取决于被改动的元素的位置。
- 例如,改动
body
元素的属性,就会发生整个DOM树的重新计算。
将元素样式
和DOM元素结合起来,就会生成Render Tree
我们可以通过devTool-Performance
来检测网站在这步所花费的时间。
布局(Layout)
计算每个可视元素 的位置信息(距离视口的距离和元素本身大小)。并生成对应的Layout Tree
。 页面上的DOM元素越多,操作就越复杂。
触发条件
对于现今富应用来讲,做页面中做元素的移动和变更那是家常便饭。以下操作就会触发对应的布局流程
- 读取与元素的大小和位置相关的属性(
offsetWidth
、offsetLeft
、getBoundingClientRect
等)。 - 写入与元素的大小和位置相关的属性,除了某些属性(例如
transform
和will-change
)。
这里,我们用一点篇幅来讲一下为何transform/will-change
能跳过布局阶段,直接进入合成阶段
。
在之前的Chromium 最新渲染引擎--RenderingNG
我们讲过,在渲染流程的最开始其实是还有一个步骤叫做animate
。
在渲染流程的图中,用不同颜色来标识该阶段可能会被不同的线程或者进程所执行。
颜色 | 所在进程/线程 |
---|---|
绿色 | 渲染进程中的主线程 |
黄色 | 渲染进程中的合成线程 |
橘色 | viz进程(也叫GPU进程) |
在某些阶段,可能会被多个地方所执行,所以该阶段可能存在多个颜色。
而animate
存在两个颜色(绿色
和黄色
)也就是这一步,会在多个地方被执行。
而让animate
能够在黄色
阶段,也就是在合成线程
中执行,就是我们使用了一些CSS3
属性
transform
opacity
filter
will-change
使用了这些属性后,会跳过前面很多的步骤,例如重新计算样式/布局/重绘
等阶段。并且,在合成线程进行数据转换后,开启GPU
的硬件加速。
就这速度,你说能不快吗。
其实,上面的内容在大部分教程中,都是一种铁打不动的定律。使用了transform/will-change
开启了GPU
硬件加速,所以性能提升了。
强制布局
强制布局(Forced Synchronous Layout
或 Forced Reflow
)是Web性能优化领域的一个术语,它指的是浏览器在能够继续处理后续操作之前,必须完成当前的布局计算。
当强制执行布局时,浏览器会
暂停JS主线程
,尽管调用栈不是空的。
有很多我们耳熟能详的操作,都会触发强制布局。
想了解更多👉触发强制布局的操作。
案例分析
javascript
div1.style.height = "200px"; // 更改元素大小
var height1 = div1.clientHeight; // 读取属性
浏览器无法在不重新计算其实际大小的情况下计算div1
的clientHeight
。在这种情况下,浏览器暂停JS执行并运行:样式以查看应该更改什么,以及布局以重新计算大小。布局不仅计算放置在div1
之前的元素,还计算放置在div1
之后的元素。现代浏览器通过优化计算,使得在每次都不必重新计算整个DOM树,但在糟糕的情况下仍然会发生。重新计算的过程称为layout shift。
浏览器尽量不在每次都强制执行布局。因此,它们对操作进行分组:
javascript
div1.style.height = "200px";
var height1 = div1.clientHeight; // <-- 布局 1
div2.style.margin = "300px";
var height2 = div2.clientHeight; // <-- 布局 2
- 在第一行上,浏览器计划了高度的更改。
- 在第二行上,浏览器收到了读取属性的请求。由于我们有挂起的高度更改,浏览器必须强制执行布局。
- 在第三 + 四行上我们有相同的情况。为了让浏览器更好地处理,我们可以组合读取和写入操作:
javascript
div1.style.height = "200px";
div2.style.margin = "300px";
var height1 = div1.clientHeight; // <-- 布局 1
var height2 = div2.clientHeight;
通过组合元素,我们摆脱了第二次布局,因为当浏览器达到第四行时,它已经有了所有的数据。
我们的事件循环从一个循环变成了多个,因为我们可以在任务和微任务阶段都强制执行布局:
优化布局
- 将读取/写入操作分组,摆脱不必要的布局
- 优先采用硬件加速
- 批量DOM更改:集中进行DOM更改,然后一次性读取布局信息,可以减少强制布局的次数。
- 避免布局抖动:布局抖动是指在一系列操作中交替进行读写操作,导致多次布局计算。通过避免布局抖动,可以改善性能。
- 使用现代CSS布局技术:如
Flexbox
和Grid
,这些技术可以提高布局性能,尤其是在动态内容变化时。 - 使用虚拟化:如在长列表中,只渲染进入视口的元素,可以极大地减少布局计算的负担。
绘制(Paint)
在Layout
阶段已经把可见元素都安排的明明白白了,现在我们就需要对其粉饰一番了。也就是对其浓墨重彩的处理下。
这个操作通常不会消耗很多时间,可能在第一次渲染时可能会很大。
合成(Composition)
合成
是默认在GPU
上运行的唯一阶段。
在这一步中,浏览器仅执行特定的CSS样式,比如transform
。(硬件加速那块介绍过了)
重要说明:transform: translate
不会在GPU上启动渲染。因此,如果我们在代码中有 transform: translateZ(0)
来将渲染移到GPU上,这样是行不通的。这是一个误解。
我们可以通过源码找到原因
在devTool
可以在GPU
的信息中看到所消耗的时间
5. EventLoop模型
通过上面的对各个节点的分析,最终EventLoop
的模型图如下所示:
伪代码表示EventLoop
javascript
// 开始一个无限循环,模拟浏览器的事件循环
while (true) {
// 记录任务开始的时间
const taskStartTime = performance.now();
// 从事件队列中取出一个任务
const task = eventQueue.pop();
// 如果任务存在,则运行该任务
if (task)
task.run();
// 如果任务运行时间超过50ms,则报告长任务
if (performance.now() - taskStartTime > 50)
reportLongTask();
// 如果没有渲染机会(可能是因为当前处于忙碌状态或频率限制),则继续下一次循环
if (!hasRenderingOpportunity())
continue;
// 调用所有排队的动画帧回调
invokeAnimationFrameCallbacks();
// 如果需要样式计算和布局,则执行它们
while (needsStyleAndLayout()) {
// 执行样式计算和布局
styleAndLayout();
// 调用所有的尺寸调整观察者回调
invokeResizeObservers();
}
// 标记绘制时间,这可能是用于性能监控
markPaintTiming();
// 执行渲染,更新屏幕上的内容
render();
}
长任务 和 50ms
上面的伪代码中,我们有两个针对于本文来讲比较陌生的词或者变量
- 长任务
- 50ms
其实,我们之前在介绍浏览器性能指标
时有过接触的。下面我就把这些罗列一下,不做过多的解释了。
我们在浏览器之性能指标-TBT中介绍过
任何超过50毫秒的任务被认为是长任务
在浏览器之性能指标-TTI中的标准中也提到过50ms
在浏览器之性能指标-INP中,我们进行INP
优化时也涉及长任务的优化处理
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。