前言
我们都知道使用requestAnimationFrame实现动画可以使得动画更加平滑流畅,那为什么呢?浏览器渲染和我们平常写的JS又是如何协调及相互影响?
Event loop 和 JS 引擎、渲染引擎的关系
JavaScript 引擎
- 定义: JavaScript 引擎负责解释和执行 JavaScript 代码。
- 作用: 当浏览器加载一个包含 JavaScript 的网页时,JavaScript 引擎会解析并执行页面上的 JavaScript 代码。常见的 JavaScript 引擎有 V8(Chrome、Node.js)、SpiderMonkey(Firefox)、JavaScriptCore(Safari)等。
- 任务: 执行 JavaScript 代码、处理异步任务、管理内存等。
- 组成: Parser ,Interpreter ,JIT Compiler ,GC(Garbage Collector)
Parser
Parser负责将JavaScript源代码解析为抽象语法树(AST)
Interpreter
Interpreter负责将AST转换为字节码,并执行这些字节码。字节码是一种类似于机器码的中间代码,由解释器逐行执行
JIT Compiler
JIT(Just-In-Time)编译器将热点代码(在运行时被频繁执行的代码)从字节码转换为本地机器码,以提高性能
JIT编译器是一个优化步骤,将部分代码转换为机器码,从而加速执行。并不是所有的JavaScript代码都会被JIT编译,只有经过运行时分析的热点代码才会被选择进行JIT编译。
为什么解释字节码而不是直接生成机器码?
一些解释字节码的优势和原因:
- 跨平台性: 字节码是一种中间表示,与特定硬件架构无关。解释字节码的方法允许同一份字节码在不同平台上运行,而无需为每个平台生成特定的机器码。这使得实现跨平台的 JavaScript 引擎更加方便。
- 即时编译(Just-In-Time Compilation,JIT): 字节码的解释执行过程相对快速,因为不需要等待整个程序编译成机器码。在运行时,JIT编译器可以根据运行时的上下文和执行路径,选择性地将热点代码转换为机器码,以提高性能。
- 优化机会: 字节码提供了更多优化的机会。由于它是一种中间表示,可以在解释和执行的过程中进行更多的分析和优化,以提高整体性能。这种优化通常包括内联缓存、函数内联、去掉未使用的代码等。
- 更快的启动时间: 直接生成机器码可能会导致较长的启动时间,因为在执行之前需要先编译整个程序。而解释字节码可以更快地启动应用程序,因为只需解释器直接执行字节码而不需要额外的编译时间。
- 动态语言支持: 字节码执行更适合动态语言,因为它们的类型和结构可能在运行时发生变化。解释器可以更灵活地处理这些动态性的特征。
总体而言,解释字节码的方式提供了一种灵活且跨平台的执行方法,并且在即时编译的场景下,允许在运行时进行更多的优化。这使得JavaScript引擎能够在不同平台和不同应用场景下取得较好的性能表现。JIT编译器提供了一种折中方案,结合了解释执行的灵活性和即时编译的性能优势。通过将热点代码编译为本地机器码,JavaScript引擎可以在运行时获得更高的性能,同时保持跨平台性和灵活性。
gc
js垃圾回收js 垃圾回收机制 | HzmBlog (huangzumao.space)
编译流水线
一般的 JS 引擎的编译流水线是 parse 源码成 AST,之后 AST 转为字节码,解释执行字节码。运行时会收集函数执行的频率,对于到达了一定阈值的热点代码,会把对应的字节码转成机器码(JIT),然后直接执行。
来源于:Event loop 和 JS 引擎、渲染引擎的关系(精致版) - 知乎 (zhihu.com)
渲染引擎
渲染引擎是浏览器中负责解析和渲染网页内容的关键组件。它将HTML、CSS、JavaScript等资源解析并转换成用户可以看到的页面。以下是渲染引擎的主要功能和工作流程:
- HTML 解析: 渲染引擎从网络或本地缓存中获取HTML文档,然后开始解析HTML代码,构建DOM树(文档对象模型)。DOM树表示了HTML文档的层次结构,包括页面的元素、属性和它们之间的关系。
- CSS 解析: 同时,渲染引擎解析CSS样式表,构建CSSOM树(CSS对象模型)。CSSOM树表示了CSS样式规则的层次结构,包括样式规则的选择器、属性和值。
- Render 树构建: 渲染引擎将DOM树和CSSOM树合并成一个Render树。Render树包含了页面中可见的节点,每个节点都与实际渲染的元素相对应,包括计算后的样式信息。
- 布局(Layout): Render树的节点包含了每个元素的位置和大小信息。渲染引擎进行布局阶段,计算每个元素在屏幕上的准确位置,这个过程也称为回流(reflow)。
- 绘制(Painting): 在布局之后,渲染引擎将每个元素绘制到屏幕上。这个过程包括填充颜色、绘制边框、渲染文本等,生成位图图层。
- 合成(Composite): 渲染引擎将各个位图图层按照正确的顺序合成,形成最终的页面。这个过程被称为合成阶段。
- 重绘和回流: 当页面发生变化时,渲染引擎可能需要进行重绘(repaint)或回流(reflow)操作。重绘只涉及颜色的变化,而回流涉及布局的变化。这两个操作都会消耗一定的性能,因此在开发中需要谨慎操作,避免不必要的重绘和回流。
- 事件处理: 渲染引擎还负责处理用户交互事件,例如鼠标点击、键盘输入等。渲染引擎负责渲染和绘制页面,但并不直接处理用户交互事件。然而,当用户触发了一个事件,例如点击了页面上的元素,渲染引擎会生成相应的事件对象,并将其传递给浏览器的事件系统
不同的浏览器使用不同的渲染引擎,例如Chrome使用的是Blink引擎,Firefox使用的是Gecko引擎。
来源于:Event loop 和 JS 引擎、渲染引擎的关系(精致版) - 知乎 (zhihu.com)
事件循环Event Loop:
谈到事件循环,有些人会以为它是js引擎提供的,其实事件循环是由 JavaScript 运行时环境提供的机制,如浏览器内核,NodeJS环境等等。
eventloop规范:HTML Standard (whatwg.org)
大概的流程是:
-
从 task 队列(可以理解是宏任务)中选出最老的一个 task,执行它。
-
清空所有的微任务
-
更新Render,但是在这里会判断是否有Rendering opportunities ,可以理解为是不是需要重新渲染,浏览器只需保证 60Hz 的刷新率即可(在机器负荷重时还会降低刷新率),若 eventloop 频率过高,即使渲染了浏览器也无法及时展示,类似的,如果一个顶层浏览器上下文在后台运行,用户代理可能决定将该页面的刷新率降到 4Hz,甚至更低。所以并不是每轮 eventloop 都会执行 UI Render。
该流程包括:(仅针对可见区域的doucument For each fully active
Document
in docs)
0. 执行各种渲染所需工作,如 触发 resize、scroll 事件、建立媒体查询、运行 CSS 动画- 执行 animationframecallbacks 也就是requestAnimationFrame
- 执行 IntersectionObserver callback
- 渲染
-
如果task为空,微任务队列没任务,还没达到需要重新渲染的时间,进行
Idle
空闲周期的算法,判断是否要执行requestIdleCallback
的回调函数,可以给
rIC
传入第二个参数timeout
,保证即使没有空闲时间,也会在这个最长等待时间过后执行 -
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