EventLoop = TaskQueue + RenderQueue

人法地,地法天,天法道,道法自然

大家好,我是柒八九 。一个专注于前端开发技术/RustAI应用知识分享Coder

前言

在最近的工作和学习中,有一个词总是在眼前挥之不去--EventLoop。而在之前,其实我们讲过相关的内容,Event Loop 可视化解析

上文我们从偏JS调用机制的角度分析了,调用栈(Call Stack)/宏任务队列 (Task Queue)和微任务队列 (Microtask Queue)他们之间的关系和他们是如何协同合作的。并且,举了很多例子,用可视化的方式讲解它们如何工作的。

而今天,我们从浏览器内部的实现细节来谈谈EventLoop是如何从接受任务到渲染出对应页面的。

也就是下图中所涉及到的各个重要节点。在阅读完本文后,希望大家能对下面有一个清晰的认知。

好了,天不早了,干点正事哇。

我们能所学到的知识点

  1. 前置知识点
  2. 事件循环(Event Loop)
  3. 任务队列/微任务队列/调用栈
  4. 在渲染队列中执行的是什么?
  5. EventLoop模型

1. 前置知识点

前置知识点 ,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略

同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履 。以下知识点,请酌情使用

页面刷新术语

我们在页面是如何生成的(宏观角度)一文中提到过这些指标,这里就拿来主义了。

  • 屏幕刷新频率
    • 一秒内屏幕刷新的次数(一秒内显示了多少帧的图像),单位 Hz(赫兹),如常见的 60 Hz刷新频率取决于硬件的固定参数(不会变的)。
  • 逐行扫描
    • 显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。
    • 60 Hz 刷新率的屏幕为例,这一过程即 1000 / 60 ≈ 16ms
    • 当扫描完一个屏幕后,设备需要重新回到第一行 以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)
  • 帧率 (Frame Rate)
    • 表示 GPU 在一秒内绘制操作的帧数 ,如 60 fps,即每秒钟GPU最多绘制 60 帧画面。
    • 帧率是动态变化 的,例如当画面静止时,GPU 是没有绘制操作的,屏幕刷新的还是buffer中的数据,即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) 使用XMLHttpRequestfetch API,可以发起异步的HTTP请求到服务器,并在不刷新页面的情况下获取或发送数据。
DOM操作 (DOM Manipulation) 浏览器提供了一套DOM API,允许JavaScript访问和操作页面上的元素。比如,可以添加、删除或更改元素,或者修改元素的样式和内容。
事件处理 (Event Handling) WebAPI允许注册事件处理程序来响应用户行为(如点击、滑动)或浏览器事件(如页面加载、窗口尺寸变化)。
存储机制 (Storage Mechanisms) 浏览器提供了如localStoragesessionStorageIndexedDB等API,可以在用户的设备上存储数据。
设备API (Device APIs) 可以访问设备的硬件,如摄像头、麦克风、地理位置等,这通常通过navigator对象暴露的API实现。
图形和动画 (Graphics & Animation) CanvasWebGL API允许在网页上绘制二维和三维图形。requestAnimationFrame为动画提供了一个优化的循环机制。
性能监控 (Performance Monitoring) Performance API提供了获取浏览器性能相关数据的接口,帮助开发者监控和优化网页性能。
其他API (Other APIs) 还有诸如Web Audio APIWebRTCWebSocket等,使得在网页上实现复杂的音频处理、实时通信成为可能。

WebAPI的工作流程

  1. 调用API :开发者在JavaScript代码中调用某个WebAPI

  2. 浏览器解释执行 : 浏览器解释JavaScript代码,并执行相应的API调用

  3. API内部处理WebAPI内部可能会执行多种操作,如触发网络请求、访问数据库、启动硬件设备等。

  4. 回调和事件循环 :对于异步操作,WebAPI通常会使用回调函数Promise来处理操作完成后的结果。浏览器的事件循环机制确保了这些回调在适当的时候被调用。

  5. 渲染和更新:对于涉及视觉变化的API,如DOM操作或Canvas绘图,浏览器会更新页面内容,这通常发生在浏览器的下一个重绘周期。

在整个过程中,浏览器的角色是中介,它提供了执行API的环境和必要的安全措施。这些API让Web应用可以像本地应用一样丰富和强大,同时仍然运行在浏览器这个相对安全的沙箱环境中。

下面的图,展示了WebAPI的地位(中间部分)。


GPU硬件加速

GPU(Graphics Processing Unit)硬件加速 是一种利用GPU来执行图形和计算任务的技术。在Web开发中,GPU硬件加速可以通过利用用户计算机中的GPU资源来加速浏览器的渲染和绘制操作。这通常可以提高网页的性能和流畅度,尤其是对于需要大量图形操作的页面。

在Web开发中,一些CSS属性和操作可以触发GPU硬件加速,以便更有效地利用GPU资源。

  1. 3D 变换(transform

    • 使用transform属性进行3D变换,如translate3drotate3dscale3d等,可以触发GPU硬件加速。例如:

      css 复制代码
      .element {
        transform: translate3d(0, 0, 0);
      }
  2. CSS 动画(animation)和过渡(transition):

    • 使用CSS动画和过渡属性,例如transform属性的过渡,可以触发GPU硬件加速。例如:

      css 复制代码
      .element {
        transition: transform 0.3s ease-in-out;
      }
  3. Canvas 绘图:

    • <canvas>元素上进行绘图操作通常会利用GPU硬件加速。这包括使用2DWebGL上下文进行图形渲染。
  4. 使用 will-change 属性:

    • will-change属性告诉浏览器某个属性将会被改变,从而可以提前进行优化。例如:

      css 复制代码
      .element {
        will-change: transform;
      }
  5. 使用 image-rendering 属性:

    • image-rendering属性用于指定图像的渲染质量,而且在某些情况下也能触发GPU硬件加速。例如:

      css 复制代码
      .element {
        image-rendering: pixelated;
      }
  6. 使用 backface-visibility 属性:

    • backface-visibility属性用于指定当元素不面向屏幕时是否可见。在某些情况下,该属性的使用可以触发GPU硬件加速。例如:

      css 复制代码
      .element {
        backface-visibility: hidden;
      }
  7. 使用 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探查并执行对应的任务,首先要做的就是将某些任务进行触发。也就是唤起指定任务的触发器。

下面就是我们平时能够接触到的任务触发器

  1. <script>标签 :通过HTML<script>标签引入的代码会被浏览器解析并执行,相关的同步任务会被放入事件循环中。

  2. 延后的任务

    • setTimeout:设置一个计时器,在指定的延时后执行一段代码。
    • setInterval:设置一个计时器,按照指定的时间间隔重复执行一段代码。
    • requestIdleCallback:安排一个函数在浏览器空闲时期被调用。
  3. 浏览器API的事件处理程序

    • 用户触发的事件,例如click, mousedown, input, blur等。
    • 代码生成的事件,比如XMLHttpRequest的响应处理、fetch API的promise resolve等。
  4. Promise状态变化 :当一个Promise对象的状态改变时(例如从pending变为fulfilledrejected),相关的任务会被加入事件循环

  5. 观察者

    • DOMMutationObserver:用于观察DOM变动,当DOM发生变化时可以通知应用。
    • IntersectionObserver:用于观察元素是否进入了父元素或视口的特定区域。
  6. requestAnimationFrame:用于在下一次重新渲染前执行动画或视觉更新的函数,使动画流畅。

上面的任务几乎都是通过WebAPI进行触发的。

例如,我们在代码中有这样一行:setTimeout(function a() {}, 100)。当我们执行setTimeout时,WebAPI任务延迟了100毫秒。100毫秒后,WebAPI将函数a()放入任务队列(Task Queue)(也可以称为Callback Queue)中。事件循环在下一个循环中获取该任务并执行它。


JS执行和页面渲染是难兄难弟

EventLoop = TaskQueue + RenderQueue

在之前的文章中,我们提到过文档对象模型(DOM)是一个应用编程接口(API),通过创建表示文档的树,以一种独立于平台和语言的方式访问和修改一个页面的内容和结构。

在HTML文档中,Web开发者可以使用JSCRUD DOM 结构,其主要的目的是动态改变HTML文档的结构。

  • 读取DOM元素的数据:大小、属性、位置等
  • 改变属性:data-属性、宽度、高度、位置、CSS属性等
  • 创建/删除HTML节点

而在JS把玩DOM之后,就将其扔给了浏览器的渲染引擎,而渲染引擎任劳任怨的处理DOMDOM携带的附带信息,并将其渲染成用户心仪的页面。

也就是说JS和浏览器(渲染引擎)都能染指过DOM

基于上面的特定的背景,我们可以得出一个结论,执行JS渲染页面都在同一个线程里。

这意味着事件循环包含渲染流程。而渲染流程不是单一的操作。其实,它是渲染队列

现在EventLoop处理链路上有两个任务源。

  1. JS任务 - SomeJsTasks
  2. 渲染任务 - RenderQueue

屏幕更新

对于浏览器来说,事件循环与帧(frames)密切相关,因为EventLoop同时执行JS代码并渲染页面。

可以将视为屏幕状态的快照,即用户在某一时刻看到的画面。

我们在Chromium 最新渲染引擎--RenderingNG也介绍过frames

浏览器的目标是尽快显示页面更新,考虑到硬件软件的限制:

  • 硬件限制:屏幕刷新率
  • 软件限制:操作系统设置、浏览器及其设置、节能设置等

绝大多数浏览器/操作系统支持60帧每秒Frames Per SecondFPS)。浏览器试图以这个特定的速率更新屏幕。

当我们使用60 FPS时,这意味着浏览器在必须渲染新帧之前有16.6毫秒的时间段来执行任务1000/60),而渲染新帧也会消耗时间。


3. 任务队列/微任务队列/调用栈

浏览器使用两个队列来执行我们的JS代码:

  • 任务队列(TaskQueue)或宏任务队列专用于所有事件延迟任务等。
  • 微任务队列(Microtask Queue)用于处理 promise 回调(已解决和已拒绝),以及 MutationObserver。这个队列中的单个元素被称为 微任务

任务队列(Task Queue)

当浏览器收到一个新任务时,它将任务放入任务队列。每个事件循环定期从任务队列中获取任务并执行它。任务完成后,如果浏览器有时间(渲染队列没有任务),事件循环从任务队列获取另一个任务,直到渲染队列接收到任务为止

案例分析1

我们有3个任务:ABC事件循环获取并执行第一个任务,花费了4毫秒。然后事件循环检查其他队列(微任务队列渲染队列),它们是空的。事件循环执行任务B,花费了12毫秒。总共两个任务使用了16毫秒。然后浏览器将任务添加到渲染队列以绘制新帧。事件循环检查渲染队列并开始执行渲染队列中的任务,它们大约花费1毫秒。完成这些操作后,事件循环返回到任务队列并执行最后一个任务C。

事件循环无法预测任务将花费多少时间。此外,事件循环无法暂停任务来渲染帧,因为浏览器引擎不知道该任务是否会对绘制内容有修改动作,还是任务只是为了渲染帧做了一些无关痛痒的准备工作。

在执行JS代码期间,JS所做的所有更改并不会直接呈现给用户,而是等到宏任务和所有待处理的微任务完成后才会表现出来 。但是,此时在JS中可以获取到最新DOM的变更信息

案例分析2

队列中只有2个任务(AB)。第一个任务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行调用栈的入口的。

当调用栈为空时,当前任务完成。


微任务

微任务只有两个可能的来源:

  1. Promise 回调(onResolved/onRejected
  2. MutationObserver 回调。

微任务有一个主要特征,使它们与其他任务完全不同:

一旦调用栈为空,微任务将立即执行。

微任务可以创建其他微任务,这些微任务将在调用栈结束时执行 。每个新的微任务都会推迟执行新的宏任务或新帧的渲染

案例分析

在这个例子中,微任务队列中有4个微任务:

要执行的第一个微任务是AA花费了200毫秒,而且我们在渲染队列中有任务。然而,它们将被推迟,因为我们仍然有3个微任务。这意味着在执行A后,事件循环将执行微任务BC,最后是D。当微任务队列变空时,事件循环渲染新帧 。在这个例子中,这4个微任务花费了0.5秒。在这段时间内,浏览器UI被阻塞,不可交互

后续的微任务可以阻塞网站UI,使页面变得不可交互。

这个微任务特性既可能是优势也可能是劣势。例如,当 MutationObserver 根据DOM更改调用其回调时,用户在回调完成之前看不到页面上的更改。因此,我们可以有效地管理用户看到的内容。

更新后的事件循环图示:

各自的特性

  • 调用栈是用于跟踪正在被执行 函数的机制,而宏任务队列是用于跟踪将要被执行函数的机制。
  • 宏任务队列微任务队列都是FIFO (先进先出)的队列结构,这些任务是同步阻塞

4. 在渲染队列中执行的是什么?

其实,在浏览器中渲染页面是有很多步骤的。

同时还涉及多个进程之间的通信。这在之前的页面是如何生成的(宏观角度)有过介绍,这里就不在罗嗦了。

而今天呢,我们从浏览器渲染帧的角度来看到底发生了啥?!其实帧渲染不是一个单一的操作,它有几个阶段,每个阶段都可以分为子阶段。

RequestAnimationFrame(RAF)

requestAnimationFrame 是一个由浏览器提供的 JavaScript API,用于在下一次浏览器重绘之前执行指定的函数。这个函数通常用于执行动画或其他需要高性能更新的任务,因为它会在浏览器的绘制周期内运行,以确保动画的平滑流畅。

特点

  1. 与浏览器的重绘同步: requestAnimationFrame 的执行时机与浏览器的重绘周期相同,通常是每秒60次(60帧每秒),这确保了动画的流畅性。

  2. 自动暂停: 当用户切换到其他标签页或最小化浏览器时,requestAnimationFrame 会自动暂停,从而节省系统资源。

  3. 避免卡顿: 由于与浏览器的绘制同步,requestAnimationFrame 可以避免由于连续执行任务导致的卡顿和性能问题。

  4. RAF的回调有一个DOMHighResTimeStamp参数,它是自时间起源以来经过的毫秒数,即文档生命周期的开始。我们不需要在回调中使用performance.now()

  5. RAF返回一个描述符(id),因此你可以使用cancelAnimationFrame取消RAF回调(就像使用setTimeout一样);

  6. 更改元素大小或读取元素属性的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元素越多,操作就越复杂。

触发条件

对于现今富应用来讲,做页面中做元素的移动和变更那是家常便饭。以下操作就会触发对应的布局流程

  • 读取与元素的大小和位置相关的属性(offsetWidthoffsetLeftgetBoundingClientRect等)。
  • 写入与元素的大小和位置相关的属性,除了某些属性(例如 transformwill-change)。

这里,我们用一点篇幅来讲一下为何transform/will-change能跳过布局阶段,直接进入合成阶段

在之前的Chromium 最新渲染引擎--RenderingNG我们讲过,在渲染流程的最开始其实是还有一个步骤叫做animate

在渲染流程的图中,用不同颜色来标识该阶段可能会被不同的线程或者进程所执行。

颜色 所在进程/线程
绿色 渲染进程中的主线程
黄色 渲染进程中的合成线程
橘色 viz进程(也叫GPU进程)

在某些阶段,可能会被多个地方所执行,所以该阶段可能存在多个颜色。

animate存在两个颜色(绿色黄色)也就是这一步,会在多个地方被执行。

而让animate能够在黄色阶段,也就是在合成线程中执行,就是我们使用了一些CSS3属性

  1. transform
  2. opacity
  3. filter
  4. will-change

使用了这些属性后,会跳过前面很多的步骤,例如重新计算样式/布局/重绘等阶段。并且,在合成线程进行数据转换后,开启GPU硬件加速

就这速度,你说能不快吗。

其实,上面的内容在大部分教程中,都是一种铁打不动的定律。使用了transform/will-change开启了GPU硬件加速,所以性能提升了。

强制布局

强制布局(Forced Synchronous LayoutForced Reflow)是Web性能优化领域的一个术语,它指的是浏览器在能够继续处理后续操作之前,必须完成当前的布局计算

当强制执行布局时,浏览器会暂停JS主线程,尽管调用栈不是空的。

有很多我们耳熟能详的操作,都会触发强制布局。

想了解更多👉触发强制布局的操作

案例分析

javascript 复制代码
div1.style.height = "200px"; // 更改元素大小
var height1 = div1.clientHeight; // 读取属性

浏览器无法在不重新计算其实际大小的情况下计算div1clientHeight。在这种情况下,浏览器暂停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布局技术:如FlexboxGrid,这些技术可以提高布局性能,尤其是在动态内容变化时。
  • 使用虚拟化:如在长列表中,只渲染进入视口的元素,可以极大地减少布局计算的负担。

绘制(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优化时也涉及长任务的优化处理


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。

相关推荐
熊的猫44 分钟前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596933 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai3 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书