动画requestAnimationFrame与浏览器事件循环

前言

我们都知道使用requestAnimationFrame实现动画可以使得动画更加平滑流畅,那为什么呢?浏览器渲染和我们平常写的JS又是如何协调及相互影响?

Event loop 和 JS 引擎、渲染引擎的关系

JavaScript 引擎

  • 定义: JavaScript 引擎负责解释和执行 JavaScript 代码。
  • 作用: 当浏览器加载一个包含 JavaScript 的网页时,JavaScript 引擎会解析并执行页面上的 JavaScript 代码。常见的 JavaScript 引擎有 V8(Chrome、Node.js)、SpiderMonkey(Firefox)、JavaScriptCore(Safari)等。
  • 任务: 执行 JavaScript 代码、处理异步任务、管理内存等。
  • 组成: ParserInterpreterJIT CompilerGC(Garbage Collector)

Parser

Parser负责将JavaScript源代码解析为抽象语法树(AST)

Interpreter

Interpreter负责将AST转换为字节码,并执行这些字节码。字节码是一种类似于机器码的中间代码,由解释器逐行执行

JIT Compiler

JIT(Just-In-Time)编译器将热点代码(在运行时被频繁执行的代码)从字节码转换为本地机器码,以提高性能

JIT编译器是一个优化步骤,将部分代码转换为机器码,从而加速执行。并不是所有的JavaScript代码都会被JIT编译,只有经过运行时分析的热点代码才会被选择进行JIT编译。

为什么解释字节码而不是直接生成机器码?

一些解释字节码的优势和原因:

  1. 跨平台性: 字节码是一种中间表示,与特定硬件架构无关。解释字节码的方法允许同一份字节码在不同平台上运行,而无需为每个平台生成特定的机器码。这使得实现跨平台的 JavaScript 引擎更加方便。
  2. 即时编译(Just-In-Time Compilation,JIT): 字节码的解释执行过程相对快速,因为不需要等待整个程序编译成机器码。在运行时,JIT编译器可以根据运行时的上下文和执行路径,选择性地将热点代码转换为机器码,以提高性能。
  3. 优化机会: 字节码提供了更多优化的机会。由于它是一种中间表示,可以在解释和执行的过程中进行更多的分析和优化,以提高整体性能。这种优化通常包括内联缓存、函数内联、去掉未使用的代码等。
  4. 更快的启动时间: 直接生成机器码可能会导致较长的启动时间,因为在执行之前需要先编译整个程序。而解释字节码可以更快地启动应用程序,因为只需解释器直接执行字节码而不需要额外的编译时间。
  5. 动态语言支持: 字节码执行更适合动态语言,因为它们的类型和结构可能在运行时发生变化。解释器可以更灵活地处理这些动态性的特征。

总体而言,解释字节码的方式提供了一种灵活且跨平台的执行方法,并且在即时编译的场景下,允许在运行时进行更多的优化。这使得JavaScript引擎能够在不同平台和不同应用场景下取得较好的性能表现。JIT编译器提供了一种折中方案,结合了解释执行的灵活性和即时编译的性能优势。通过将热点代码编译为本地机器码,JavaScript引擎可以在运行时获得更高的性能,同时保持跨平台性和灵活性。

gc

js垃圾回收js 垃圾回收机制 | HzmBlog (huangzumao.space)

编译流水线

一般的 JS 引擎的编译流水线是 parse 源码成 AST,之后 AST 转为字节码,解释执行字节码。运行时会收集函数执行的频率,对于到达了一定阈值的热点代码,会把对应的字节码转成机器码(JIT),然后直接执行。

来源于:Event loop 和 JS 引擎、渲染引擎的关系(精致版) - 知乎 (zhihu.com)

渲染引擎

渲染引擎是浏览器中负责解析和渲染网页内容的关键组件。它将HTML、CSS、JavaScript等资源解析并转换成用户可以看到的页面。以下是渲染引擎的主要功能和工作流程:

  1. HTML 解析: 渲染引擎从网络或本地缓存中获取HTML文档,然后开始解析HTML代码,构建DOM树(文档对象模型)。DOM树表示了HTML文档的层次结构,包括页面的元素、属性和它们之间的关系。
  2. CSS 解析: 同时,渲染引擎解析CSS样式表,构建CSSOM树(CSS对象模型)。CSSOM树表示了CSS样式规则的层次结构,包括样式规则的选择器、属性和值。
  3. Render 树构建: 渲染引擎将DOM树和CSSOM树合并成一个Render树。Render树包含了页面中可见的节点,每个节点都与实际渲染的元素相对应,包括计算后的样式信息。
  4. 布局(Layout): Render树的节点包含了每个元素的位置和大小信息。渲染引擎进行布局阶段,计算每个元素在屏幕上的准确位置,这个过程也称为回流(reflow)。
  5. 绘制(Painting): 在布局之后,渲染引擎将每个元素绘制到屏幕上。这个过程包括填充颜色、绘制边框、渲染文本等,生成位图图层。
  6. 合成(Composite): 渲染引擎将各个位图图层按照正确的顺序合成,形成最终的页面。这个过程被称为合成阶段。
  7. 重绘和回流: 当页面发生变化时,渲染引擎可能需要进行重绘(repaint)或回流(reflow)操作。重绘只涉及颜色的变化,而回流涉及布局的变化。这两个操作都会消耗一定的性能,因此在开发中需要谨慎操作,避免不必要的重绘和回流。
  8. 事件处理: 渲染引擎还负责处理用户交互事件,例如鼠标点击、键盘输入等。渲染引擎负责渲染和绘制页面,但并不直接处理用户交互事件。然而,当用户触发了一个事件,例如点击了页面上的元素,渲染引擎会生成相应的事件对象,并将其传递给浏览器的事件系统

不同的浏览器使用不同的渲染引擎,例如Chrome使用的是Blink引擎,Firefox使用的是Gecko引擎。

来源于:Event loop 和 JS 引擎、渲染引擎的关系(精致版) - 知乎 (zhihu.com)

事件循环Event Loop:

谈到事件循环,有些人会以为它是js引擎提供的,其实事件循环是由 JavaScript 运行时环境提供的机制,如浏览器内核,NodeJS环境等等。

eventloop规范:HTML Standard (whatwg.org)

大概的流程是:

  1. 从 task 队列(可以理解是宏任务)中选出最老的一个 task,执行它。

  2. 清空所有的微任务

  3. 更新Render,但是在这里会判断是否有Rendering opportunities ,可以理解为是不是需要重新渲染,浏览器只需保证 60Hz 的刷新率即可(在机器负荷重时还会降低刷新率),若 eventloop 频率过高,即使渲染了浏览器也无法及时展示,类似的,如果一个顶层浏览器上下文在后台运行,用户代理可能决定将该页面的刷新率降到 4Hz,甚至更低。所以并不是每轮 eventloop 都会执行 UI Render

    该流程包括:(仅针对可见区域的doucument For each fully active Document in docs
    0. 执行各种渲染所需工作,如 触发 resize、scroll 事件、建立媒体查询、运行 CSS 动画

    1. 执行 animationframecallbacks 也就是requestAnimationFrame
    2. 执行 IntersectionObserver callback
    3. 渲染
  4. 如果task为空,微任务队列没任务,还没达到需要重新渲染的时间,进行 Idle 空闲周期的算法,判断是否要执行 requestIdleCallback 的回调函数,

    可以给 rIC 传入第二个参数 timeout,保证即使没有空闲时间,也会在这个最长等待时间过后执行

  5. worker事件响应

用一张图来概括整体流程:(来源于:深入探究 eventloop 与浏览器渲染的时序问题 - 404Forest

task(macrotask)

何为 task?task 又称 macrotask。我们查看 HTML 规范中 task 的有关章节 webappapis.html#concept-task

一个 eventloop 有一或多个 task 队列。每个 task 由一个确定的 task 源提供。从不同 task 源而来的 task 可能会放到不同的 task 队列中。例如,浏览器可能单独为鼠标键盘事件维护一个 task 队列,所有其他 task 都放到另一个 task 队列。通过区分 task 队列的优先级,使高优先级的 task 优先执行,保证更好的交互体验。

task 源包括:(webappapis.html#generic-task-sources

  • DOM 操作任务源:如元素以非阻塞方式插入文档
  • 用户交互任务源:如鼠标键盘事件。用户输入事件(如 click) 必须使用 task 队列
  • 网络任务源:如 XHR 回调
  • history 回溯任务源:使用 history.back() 或者类似 API

此外 setTimeout、setInterval、IndexDB 数据库操作等也是任务源。总结来说,常见的 task 任务有:

  • 事件回调
  • XHR 回调
  • IndexDB 数据库操作等 I/O
  • setTimeout / setInterval
  • history.back

requestAnimationFrame的优势

与浏览器的绘制过程同步,以提供最佳的性能和动画效果。

在事件循环中,可以看待在渲染帧中看到,在最后更新UI前,会执行requestAnimationFrame,使得与绘制同步,使得动画更加流畅。

为什么setTimeout不行会有卡顿感?常常会觉得setTimeout太慢了,但恰恰相反因为setTimeout属于task,如果不拥挤,在一个event loop上,是会执行多次task而不执行渲染,也就是说,动画变化太快,但是渲染跟不上,因此requestAnimationFrame可以和渲染同步。

参考文献

深入探究 eventloop 与浏览器渲染的时序问题 - 404Forest

深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示) - 掘金 (juejin.cn)

Event loop 和 JS 引擎、渲染引擎的关系(精致版) - 知乎 (zhihu.com)

chatgpt

相关推荐
Watermelo61711 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_7482489413 分钟前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_7482356124 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript