揭开事件循环的神秘面纱
创始人
2024-01-09 11:52:01
0

原标题:揭开事件循环的神秘面纱

作者 | 小萱

导读

introduction

这篇文章会全方位讲解事件循环机制,从这篇文章你可以学到,「事件循环」和「浏览器渲染」的关系,浏览器setTimeout、requestAnimationFrame(RAF)、requestIdleCallback(RIC)等API在事件循环的「执行时机」,导致浏览器卡顿的原因、交互指标是如何测量的以及如何提升网站的交互性能。

全文10503字,预计阅读时间27分钟。

GEEK TALK

01

前言

我们常常会提到页面性能,为什么要优化长任务,又为什么React要做时间切片呢。这篇文章把浏览器的渲染、事件循环与页面性能串联起来。

从这篇文章你可以学到,「事件循环」和「浏览器渲染」的关系,浏览器setTimeout、

requestAnimationFrame(RAF)、requestIdleCallback(RIC)等API在事件循环的「执行时机」,导致浏览器卡顿的原因、交互指标是如何测量的以及如何提升网站的交互性能。

学完这些,你可以对为什么动画要用RAF、又何时去用RIC、该不该选择setTimeout、如何规避长任务之类的问题应对自如。

GEEK TALK

02

事件循环概述

2.1 为什么要了解事件循环?

深入了解事件循环是性能优化的基础。在讨论事件循环之前,我们需要先了解浏览器的多进程和多线程架构。

2.2浏览器的架构

回顾浏览器的架构,现代浏览器都是多进程和多线程的。

2.2.1 多进程

Chrome浏览器使用多进程架构,意味着每个标签页(在某些浏览器中也包括每个扩展程序)通常在其自己的进程中运行。这样做的好处是,一个标签页崩溃不会影响到其他标签页。

站点隔离特性,浏览器每个tab,都是独立的渲染进程,这点的好处是假设你打开三个标签页,一个标签卡死不影响其他两个。但如果三个标签共用一个进程,一个卡死会导致全部都卡,这样体验很差。

△浏览器的多进程示意图

2.2.2 多线程

每个浏览器进程都可以包含多个线程。例如,主线程用于执行 Java 代码和处理页面布局,而其他线程可能用于网络请求、渲染等任务。

主线程

Web 应用程序需要在此单个主线程上执行某些关键操作。当您导航到 Web 应用程序时,浏览器将创建并向您的应用程序授予该线程,以便您的代码在其上执行。

主线程指的是渲染进程下的主线程,负责解析HTML、计算CSS样式、执行Java、计算布局、绘制图层等任务。

△主进程即渲染进程包含的线程图

某些任务必须在主线程上运行。例如,任何直接需要访问 DOM(即 DOM document)的操作都必须在主线程上运行(因为 DOM 不是线程安全的)。这将包括大多数 UI 相关代码。

主线程上一次只能运行一个任务

此外,一个任务必须在主线程上运行完成,然后才能运行另一个任务。浏览器没有“部分”执行任务的机制,每个任务都完整地运行直至完成。

在下面的示例中,在浏览器展示界面的时候,按顺序运行下面的任务,并且每个任务都在主线程上完成:

GEEK TALK

03

事件循环的具体流程

我们这里主要讨论的是 window event loop。也就是浏览器一个渲染进程内主线程所控制的 Event Loop。

△发生一次事件循环的具体流程

发生一次事件循环,也就是浏览器一帧中可以用于执行JS的流程如下:

从task queue取出一个task(宏任务)执行并删除 -> 执行并清空队列中全部job(微任务) -> requestAnimationFrame -- 浏览器更新渲染 -- requestIdleCallback

3.1 更新渲染的步骤

前两个步骤,耳熟能详,这里不再讨论,重点讨论「更新渲染」之后的步骤。

1. Rendering opportunities: 标志是否一次事件循环后会发生渲染。在每次事件循环的结束,不一定会发生渲染。导致不渲染的可能:无法维持当前刷新率、浏览器上下文不可见、浏览器判断更新不会造成视觉改变并且raf的回调为空。

如果这些条件都不满足,当前文档不为空,设置 hasARenderingOpportunity 为 true。

2.如果窗口变化,执行resize。

3.如果滚动,执行scroll。

4.媒体查询。

5.canvas 。

6.执行RAF回掉,传递回掉参数DOMHighResTimeStamp,开始执行回调的时间。

7.重新执行Layout等计算,渲染绘制界面。

8.如果满足 任务队列和微任务队列都为空,并且渲染时机hasARenderingOpportunity为false,执行算法是否执行requestIdleCallback 的回调函数。

3.2 执行顺序与渲染

来一道简单的题目,将创建宏任务、微任务、RIC、RAF的代码同时定义,输出执行顺序。

console.log('开始执行'); console.log('start'); setTimeout(=>{ console.log('setTimeout'); }, 0);

requestAnimationFrame(=>{console.log('requestAnimationFrame');});newPromise((resolve, reject) =>{console.log('Promise');resolve('promise resolved');})

requestIdleCallback(=>{console.log('requestIdleCallback');});

(asyncfunctionasyncFunction() {console.log(await'asyncFunction');});

console.log('执行结束');// 开始执行// Promise// 执行结束// promise resolved// asyncFunction// setTimeout// requestAnimationFrame// requestIdleCallback

你可能会疑问为什么RAF会在setTimeout(fn, 0)之前执行,setTimeout(fn, 0)的执行时机是延迟0-4ms,RAF可以粗暴理解为settimeout(fn, Math.random * 16.6),因此setTimeout会优先。但如果在setTimeout执行之前主线程被其他的任务跑满了,超过了一帧的耗时,setTimeout会在RAF的回调之后执行(用例见下面的代码段),因此setTimeout的延迟时间并不稳定,RAF的执行时机稳定,在一帧内注册的,都会在这一帧的结束,下一帧的开始之前执行。

lettask = newArray(10000).fill(null).map((_, i) =>=> {constspan = document.("span");span.innerText = i;console.log("==>task", i);});task.forEach((i) =>i);requestAnimationFrame(=>{console.log("===>requestAnimationFrame");});setTimeout(=>{console.log("===>setTimeout");}, 0);//输出:// ===>requestAnimationFrame// ===>setTimeout

注意,Promise.then的回调可以保证第一轮的准确性,如果继续.then发生的行为和浏览器版本有关,开发时不要过分依赖多.then的回调顺序,这是不可靠的。

上面提到渲染是在一次事件循环的「最后」发生,那么对于多次「修改dom」的操作,是会被合并取最后一次的结果作为布局渲染。

constbtn = document.querySelector(".btn");btn.addEventListener("click", =>{box.style.transform = "translateX(400px)";box.style.transition = "transform 1s ease-in-out";box.style.transform = "translateX(200px)";});

外层父容器400px,这段代码,表现是盒子从0到200px,盒子设置400px的动作,被合并掉了。那如何实现盒子从400px呢,可以采取延迟到下一帧渲染。

△演示效果

btn.addEventListener("click", =>{box.style.transform = "translateX(400px)";requestAnimationFrame(=>{requestAnimationFrame(=>{box.style.transition = "transform 1s ease-in-out";box.style.transform = "translateX(200px)";});});});

「嵌套的RAF」可以保证回调在下一帧执行。当然,此处用setTimeout也可以达到同样的延迟效果。

△延迟后的演示效果

GEEK TALK

04

任务队列与执行时机

执行 Java task 是在渲染之前,如果在一帧之内 Java 执行时间过长就会阻塞渲染,同样会导致丢帧、卡顿,这里的js执行时间过长,就是长任务,下面会仔细介绍。

对长任务的定义:如果任务耗时超过50ms,则认为该任务是长任务。

当我们谈到长任务造成页面卡顿时,通常指的是主线程(Main Thread)上的任务。主线程指的是渲染进程下的主线程,负责解析HTML、计算CSS样式、执行Java、计算布局、绘制图层等任务。当主线程上的一个任务(例如一个Java函数)运行时间过长时,它会阻塞主线程上的其他任务,包括但不限于UI更新和用户交互事件的处理,从而导致页面卡顿或不响应。

JS的执行和渲染的关系:

JS执行与Paint任务都发生在主线程,具体的绘制操作是交由合成线程完成,与主线程并不互斥,但是JS的执行时间过长,会导致Paint整理好的数据没有及时提交给合成线程,因此页面有帧没有执行绘制,也就是掉帧。

△JS的执行和渲染的关系图

4.1 为什么不使用setTimeout做动画

raf和setTimeout对比:

(https://jsfiddle.net/hixuanxuan/mrw6upgs/3/

1.不同步与显示刷新率:

浏览器通常以每秒60帧的速度刷新,大约每16.67毫秒刷新一次。如果你使用setTimeout来创建动画,并尝试每16.67毫秒运行一帧,你的代码不会完全与浏览器的刷新速率同步,导致丢帧

2.延迟执行:

setTimeout的延迟时间参数只是一个最小延迟时间,而不是保证执行的精确时间。如果主线程忙于其他任务,setTimeout的回调可能会被延迟,导致丢帧

3.计时器合并:

浏览器渲染有渲染时机(Rendering opportunity),也就是浏览器会根据当前的浏览上下文判断是否进行渲染,因为考虑到硬件的刷新频率限制、页面性能以及页面是否存在后台等等因素,宏任务之间不一定会伴随着浏览器绘制。如果两个Task距离的很近,他们可能会被合并在一次渲染任务,得到的结果是意料之外的,如果Task距离较大,那他跟不上浏览器的刷新频率,会导致丢帧。

RAF的执行时机是在下一次渲染前调用,也就是说使用这个API允许你在下一次渲染开始之前更改DOM,然后在本次渲染中立即体现,因此他是制作动画的绝佳选择。

4.2 requestIdleCallback的执行时机

主要在浏览器的主线程空闲时执行,为了保证响应性,会计算一个截止时间,computeDeadline,它将决定何时执行 requestIdleCallback 中注册的回调。下面是计算截止时间算法的简要概述:

1.设置初始截止时间:

初始化时,将事件循环的最后闲置周期开始时间设置为当前时间。

设置一个基本的截止时间,该时间是事件循环的最后闲置周期开始时间加上50毫秒(为了保证对新用户输入的响应性)。为什么要加这个50ms,是因为浏览器为了提前应对一些可能会突发的用户交互操作,比如用户输入文字。如果给的时间太长了,你的任务把主线程卡住了,那么用户的交互就得不到回应了。50ms 可以确保用户在无感知的延迟下得到回应。

2.检查是否有待处理的渲染:

初始化一个变量 hasPendingRenders 为 false。

遍历相同事件循环的所有窗口,检查每个窗口是否有未执行的RAF回调或可能的渲染更新。如果有,将 hasPendingRenders 设置为 true。

3.基于timeout调整截止时间:

如果 RIC 传入第二个参数 timeout,更新截止时间为timeout。这会强制浏览器不管多忙,都在超过这个时间之后去执行 rIC 的回调函数。

4.考虑渲染的时间:

如果 hasPendingRenders 为 true,计算下一个渲染的截止时间,基于事件循环的最后渲染机会时间和当前的刷新率。

如果下一个渲染的截止时间早于当前设置的截止时间,那么更新截止时间为下一个渲染的截止时间。

5.返回最终的截止时间:

返回计算出的截止时间,这个时间将用于确定何时执行 requestIdleCallback 中注册的回调。

6.开始空闲期:

对于相同事件循环的每个窗口,执行“开始空闲期”算法,使用 computeDeadline 作为参数,确定何时执行 requestIdleCallback 中注册的回调。

也就是说,这个 timeRemaining 的计算非常动态,会根据上面这些因素去决定。

4.3 React如何实现Time slice,没有使用RIC、setTimeout的原因是什么

没使用RIC的原因是他在部分浏览器表现不佳,比如safari。

需要满足的条件:

1.暂停 JS 执行,将主线程去执行style、layout、paint等任务,让浏览器有机会更新页面。

2.在未来某个时刻可以继续调度任务,执行上次还没有完成的任务。

对于react的Time Slice,他的目的是中断当前js的执行,让他去执行渲染相关任务,因此需要的API是在浏览器的Paint之后执行,浏览器并未提供除了RIC这样的API。RAF的执行时机是在一帧的结束,此时创建宏任务开启下一轮Task,渲染的任务放在RAF里在这一帧执行。如果使用setTimeout(fn, 0)创建宏任务,如果timeout嵌套的层级超过了 5 层,最低会有4ms的延迟,具体定义的代码可以参考chrome对计时器的定义(https://chromium.googlesource.com/chromium/blink/+/master/Source/core/frame/DOMTimer.cpp),因此首选的是message channel,优先级高于setTimeout可以在上一帧渲染结束后立即执行,这样就实现了可以中断的JS执行的效果

4.4 模拟实现requestIdecallback

要模拟实现requestIdecallback的效果,定义的任务队列在浏览器完成渲染任务之后执行,扩展来说也可以用来测量浏览器渲染任务的执行时间。

Background Tasks API - Web API 接口参考 | MDN(https://developer.mozilla.org/zh-CN/docs/Web/API/Background_Tasks_API)

// 当到时间了,立即执行的函数constperformWorkUntilDeadline = =>{if(scheduledHostCallback !== null) {constcurrentTime = getCurrentTime;// 分配任务的剩余时间,这个可执行时间是根据fps动态算的deadline = currentTime + yieldInterval;consthasTimeRemaining = true;// 调用已计划的回调,并传递剩余时间和当前时间。consthasMoreWork = scheduledHostCallback(hasTimeRemaining,currentTime,);if(!hasMoreWork) {isMessageLoopRunning = false;scheduledHostCallback = null;} else{// If there's more work, schedule the next message event at the end// of the preceding one.port.postMessage(null);}} else{isMessageLoopRunning = false;}// 给浏览器一个绘制的机会,并重置需要绘制的标志。needsPaint = false;};constchannel = newMessageChannel;constport = channel.port2;channel.port1.onmessage = performWorkUntilDeadline;

requestHostCallback = function(callback) {scheduledHostCallback = callback;if(!isMessageLoopRunning) {isMessageLoopRunning = true;port.postMessage(null);}};

GEEK TALK

05

交互性能指标与优化方法

长任务对页面的影响,带来「卡顿」、「掉帧」等不好的体验,常用衡量交互性能的指标有TTI和FID,这些均可使用web-vital库进行测量。下面展开对指标的详细介绍。

5.1 交互性能的衡量指标

衡量交互性能的指标主要关注以下几个方面:

5.1.1 TTI (理想可交互时间)

1.定义可交互:

首先,需要明确什么是“可交互”。一个页面被认为是可交互的,意味着页面的主要内容已经加载完毕,用户可以进行点击、输入等交互操作,而且页面能够快速响应。

2.监测首次内容绘制 (FCP) 和 DOMContentLoaded:

测量TTI的过程通常开始于监测首次内容绘制 (FCP) 和 DOMContentLoaded 事件。这两个事件分别表示浏览器开始绘制页面内容和DOM结构加载完毕的时刻。

3.长任务监测:

长任务是指那些执行时间超过50毫秒的任务。长任务通常会阻塞主线程,延迟页面的交互可用性。通过监测长任务,可以了解主线程何时变得空闲。

4.寻找交互窗口:

为了确定TTI,需要找到一个至少5秒钟主线程空闲的窗口,且该窗口应在首次内容绘制 (FCP) 之后。在这个5秒空闲窗口期间,没有长任务执行,意味着用户可以与页面交互。一旦找到这个空闲窗口,记录TTI。如果未找到长任务,则TTI与 FCP 相同。

△TTI测量示意图(源于web.dev)

5.1.2 FID(首次输入延迟)

FID,即 First Input Delay,用于量化用户在页面加载时首次交互的响应延迟。一个低的FID表示页面是快速响应用户交互的,而一个高的FID表示页面在响应用户交互时有延迟。

1.事件监听:

为了计算FID,浏览器需要监听用户的交互事件,如点击、键盘输入或者触摸事件。当用户与页面交互时,会触发这些事件。

2.事件处理时间:

当事件被触发时,浏览器会计算从事件触发到浏览器开始处理事件的时间。这个时间就是FID。它包括了浏览器将事件放入事件队列、事件队列的等待时间、以及浏览器开始处理事件的时间。

3.事件处理:

一旦事件开始被处理,浏览器会记录下处理开始的时间。如果页面在处理事件时非常忙碌,或者有其他高优先级的任务,那么事件处理可能会被延迟,这会增加FID。

5.1.3 INP(交互到下一次绘制)

INP,即Interaction to Next Paint,主要关注的是用户交互(如点击、滚动或按键操作)到页面响应的时间长度,具体到页面上的某个元素的可视更新。

比起来FID关注的是页面加载完成后用户首次交,INP 关注的是所有交互的最长渲染延迟,因此INP 不仅仅代表第一印象,可以全面评估响应情况, 使INP 比 FID在衡量用户交互体验上更为可靠。

INP将会在2024年3月取代FID成为标准性能指标。

△交互到绘制的时间

5.2 如何优化交互性能指标

1、拆分任务,这是避免长任务的有效手段。

  • 利用performance进行分析,找出long task
  • 针对long task,进行每个步骤的任务拆分,执行优先级高的,剩下的部分利用延迟代码执行的方法进行中断。

比如,有个Input框,当输入的内容发生变更,需要进行大量计算/创建dom等耗时操作,造成输入卡顿。因此我们需要在用户「尝试发生互动」的时候,「退让主线程」。

// 通过Promise实现中断后继续执行,setTimeout调用来延迟任务functionyieldToMain() {returnnewPromise(resolve=>{setTimeout(resolve, 0);});}asyncfunctionsaveSettings(tasks) {letdeadline = performance.now + 50;

while(tasks.length > 0) {// 判断当前是否有用户交互,isInputPending Chrome87+支持。// 可以采用判断Expire Time达到类似效果if(navigator.scheduling?.isInputPending ||performance.now >= deadline) {// 如果有,退让主线程,等主线程任务完成再回来继续执行。awaityieldToMain;deadline = performance.now + 50;continue;}consttask = tasks.shift;task;}}

constperformLongTask = =>{// 创建耗时的任务lettask = newArray(10000).fill(null).map((_, i) =>=> {constspan = document.("span");span.innerText = i;});saveSettings(task); // 任务切片};input.addEventListener("input", (e) => {input.value = e.target.value;performLongTask;});

2、非关键模块 延迟执行。对于点击率不高、非核心模块等,采取dynamic import的方式,用到了再加载,或是延迟到一定时间后再加载,减少首次主线程所需要执行的任务。

3、对于视口内不可见的内容,延迟加载。

  • 图片的延迟加载。
    • 为img标签loading设为lazy,延迟加载资源,直到资源达到与视口的计算距离,Chrome77+支持。
    • 利用IntersectionObserver监测图片是否在可视区域,再进行渲染。推荐使用lazy-load-image-component(https://www.npmjs.com/package/react-lazy-load-image-component)等库。
  • 减少大量dom的渲染。使用 content-visibility 延迟渲染屏幕外元素,Chrome85+支持。

4、灵活的缓存策略。

  • 用service-worker跨站资源共享。

除了资源可以采取强缓存+协商缓存配合的方式,用service-worker实现更为灵活的缓存策略。比如站点a和站点b仅满足同源,技术栈渲染方式都完全不同,如何实现在访问a的时候可以预取b的资源。站点a空闲的时候注册service-worker,访问站点b即可从cache里读取缓存,提升加载速度。sw不仅在缓存方面表现优秀,也可以帮我们实现离线应用,以及无法被浏览器强缓存的文件手动添加缓存(不同浏览器对可以强缓存的文件的体积限制不同)。

△使用sw做跨站资源预取

GEEK TALK

06

总结

1.浏览器是多进程和多线程的,通常说主线程指的是渲染进程下的主线程。

2.主线程上一次只能运行一个任务,浏览器的绘制和主线程并不互斥,但长任务会导致延迟进入合成,甚至在这一帧不发生合成也就是掉帧。

3.在每次事件循环的结束,不一定会发生渲染。setTimeout的执行时机并不稳定。

4.RAF的执行时机稳定是在当前帧的最后,下一帧的开始之前,非常适合做动画。

5.RIC的执行时机并不稳定,computeDeadline由被多因素影响计算得出,但可以传递timeout控制执行的deadline。

6.用TTI和FID(INP)去衡量页面的交互性能。

7.用长任务拆分、延迟非关键模块执行、延迟非可视区域图片加载、减少页面渲染以及配置灵活的缓存策略等手段,提升网站的交互性能。

END

参考资料:

[1]HTML living standand - evnet loop processing model:

https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model

END

2023 年,PHP 停滞不前

这里有最新开源资讯、软件更新、技术干货等内容

点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦~

相关内容

热门资讯

国产固态电池预量产,燃油车面临... 国内新能源汽车市场近年来迎来了前所未有的迅猛发展,众多品牌通过创新路径,在不同细分领域取得了显著成就...
华为 Pura80 系列发布:... 日前,华为在上海发布了一系列全场景新品,再次展现了其在智能终端领域的硬核实力。除了备受瞩目的 Pur...
福州市首个!电动车可以无线充电... 听说过手机无线充电 但你听说过 电动自行车无线充电吗? 在台江区茶亭街道广达路与群众路交汇口,福州市...
曾经被骂的很惨,新机靠“至美超... 众所周知,一提到联想这个品牌,英文叫moto,相信很多人脑海中自然浮现出三字真言“搅局者”,是真搅局...
美“星舰”第八次试飞失败系一“... 据央视新闻消息,当地时间12日,美国联邦航空管理局表示,对美国太空探索技术公司(SpaceX)的“星...
环球时报研究院邀请多位专家聚焦... 【环球时报报道 记者 马俊】编者的话:2025年被视为AI应用大规模落地的元年。AI技术带来革命性便...
国产固态电池预量产,燃油车地位... 国内新能源汽车市场近年来取得了飞速进展,众多车企通过探索不同路径,在各自的赛道上取得了显著成就。数据...
《共建“一带一路”科技创新共同... 中新社成都6月12日电 (记者 孙自法 王利文)第二届“一带一路”科技交流大会11日在四川成都开幕,...
新一代电梯能源回收装置正式亮相... 6月12日,2025武汉国际智慧物业博览会在汉开幕,作为国内领先的绿色能源解决方案提供商,梯能科技(...
触目惊心!印度坠机现场宛如“地... 惊天内幕?印度坠机“地狱”背后,究竟隐藏着哪些“隐情”?! 2025年6月12日,印度发生的客机坠毁...
双线谋局,蚂蚁缘何青睐稳定币 北京商报讯(记者 刘四红)稳定币领域再迎巨头重磅动作。6月12日,北京商报记者获悉,蚂蚁数科已经启动...
造芯片很难吗!董明珠:我不要国... 今日珠海零边界集成电路公司发生工商变更,令大家关注的是董明珠卸任法定代表人,由李斌接任法定代表人和执...
全球首个具身智能机器人4S店来... 今年8月,全球首个具身智能机器人4S店将在北京正式开放。记者刚刚从新闻发布会上获悉,该4S店将落地北...
我国科学家发起国际子午圈大科学... 我国科学家发起国际子午圈大科学计划 构建空间天气全天候“监测圈” 沿东经120度、西经60度两条经...
未来网评:网络文明潮涌,青春力... 加强网络文明建设是加快适应信息技术迅猛发展新形势的必然要求,是建设文化强国、网络强国的应有之义。6月...
上海人工智能研究院沈灏:成都有... 封面新闻记者 边雪 人工智能正以前所未有的速度重塑全球发展格局,在这一浪潮中,中国展现出了令人瞩目的...
工业互联网产业联盟:工业互联网... 今天分享的是:工业互联网产业联盟:工业互联网应用案例集(2023-2024年) 报告共计:369页 ...
前沿产品直达消费者 全球首个具... 本报记者 丁蓉 6月11日,北京亦庄宣布,全球首个具身智能机器人4S店将于2025世界机器人大会期间...
长春市中小学“科普专家进校园”...   6月12日,由长春市科学技术协会、中国科学院长春分院、长春市教育局共同主办,长春市第一实验银河小...
箭牌家居获得实用新型专利授权:... 证券之星消息,根据天眼查APP数据显示箭牌家居(001322)新获得一项实用新型专利授权,专利名为“...