前言

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

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

JavaScript 引擎

  • 定义: JavaScript 引擎负责解释和执行 JavaScript 代码。

  • 作用: 当浏览器加载一个包含 JavaScript 的网页时,JavaScript 引擎会解析并执行页面上的 JavaScript 代码。常见的 JavaScript 引擎有 V8(Chrome、Node.js)、SpiderMonkey(Firefox)、JavaScriptCore(Safari)等。

  • 任务: 执行 JavaScript 代码、处理异步任务、管理内存等。

  • 组成: ParserInterpreterCompilerGC(Garbage Collector)

Parser

(V8 Ignition)
Parser解析器将 JavaScript 源码解析为 AST,解析过程分为词法分析和语法分析,V8 通过预解析提升解析效率, 预解析,他会跳过一些未被使用的代码,比如函数的声明。在这个过程,他仅仅解析语法、声明的作用域等

Interpreter

一边解释一边执行
解释器 (V8 Ignition) 根据 AST 生成字节码并执行。这个过程中会收集执行反馈信息,交给 Compiler 进行优化编译;

Compiler

优化编译器
(V8 TurboFan)
当 V8 认为某个函数的执行频率足够高时,TurboFan 会介入,将字节码和收集到的信息作为输入,生成优化后的机器码,以提高性能
如果TurboFan基于某些假设进行的优化在运行时不再成立,V8会触发去优化。 去优化会撤销TurboFan所做的某些优化,将执行控制权交还给基线编译器或Ignition。

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

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

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

一些V8优化

  1. 尾调用优化:尾调用优化是指函数的最后一步是调用另一个函数(尾调用优化要求被调用的函数不依赖于外层函数的任何状态),这样的调用称为尾调用。尾调用优化可以避免不必要的函数调用栈的增长,提高性能和内存使用率。V8 会对尾调用进行优化,将其转换为跳转指令,复用一个上下文,以避免不必要的函数调用栈的增长。
  2. 内联缓存:内联缓存是一种缓存机制,用于提高动态语言的函数调用性能。在动态语言中,函数调用时需要进行动态类型检查,以确定调用的是哪个函数。内联缓存会缓存函数调用的类型信息,以避免重复的类型检查。对象属性的访问非常频繁,内联缓存通过缓存这些属性的查找结果。V8 会对函数调用进行内联缓存,以提高性能。
  3. 隐藏类:隐藏类是一种缓存机制,用于提高动态语言的属性访问性能。在动态语言中,对象的属性可以在运行时动态添加和删除,这会导致属性的偏移量发生变化,从而导致属性的访问性能下降。隐藏类会缓存属性的偏移量,以避免重复的偏移量计算。V8 会对对象属性进行隐藏类优化,以提高性能。
    • 具有相同结构(相同属性定义顺序相同)的对象共享相同的 Hidden Class
    • 默认情况下,添加的每个新命名属性都会导致创建一个新的 Hidden Class。
    • 添加数组索引属性不会创建新的 Hidden Class。

gc

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

编译流水线

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

image-20240110175152380

来源于: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引擎。

image-20240110183440696

来源于: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

    1. 执行各种渲染所需工作,如 触发 resize、scroll 事件、建立媒体查询、运行 CSS 动画
    2. 执行 animationframecallbacks 也就是requestAnimationFrame
    3. 执行 IntersectionObserver callback
    4. 渲染
  4. 如果task为空,微任务队列没任务,还没达到需要重新渲染的时间,进行 Idle 空闲周期的算法,判断是否要执行 requestIdleCallback 的回调函数,

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

  5. worker事件响应

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

image-20240111140555426

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