近期文章:
- Nginx Upstream了解一下
- 实现篇:一文搞懂Promise是如何实现的
- 实现篇:如何手动实现JSON.parse
- 实现篇:如何亲手定制实现JSON.stringify
- 一文搞懂 Markdown 文档规则
经常做网页动画的童鞋,对requestAnimationFrame
应该是很熟悉的,都说比setTimeout好,常常不知道具体好在哪里,今天这篇就从底层渲染开始讲清楚。随着人们对丰富、交互式 Web 体验的需求不断增长,这给 Web 前端开发带来了越来越大的压力,希望能提供流畅高效的动画效果。虽然传统的 JavaScript 定时机制(如 setInterval
和 setTimeout
)一直被用于此目的,但它们在提供最佳性能方面往往有所欠缺,导致动画卡顿和资源利用效率低下。requestAnimationFrame
作为现代解决方案应运而生,它为 Web 动画提供了一种浏览器优化的方法。
1 理解浏览器渲染管线
在理解 requestAnimationFrame
的性能优势之前,首先必须理解浏览器渲染管线,即浏览器将 HTML、CSS 和 JavaScript 代码转换为屏幕上的可视输出所执行的一系列步骤。此管线涉及几个关键阶段,每个阶段都对最终渲染的帧做出贡献。
解析
该过程始于HTML 解析和 DOM 构建。当用户导航到网页时,浏览器会收到 HTML 文档作为文本文件。然后,HTML 解析器将此文本转换为文档对象模型 (DOM),这是一个表示文档内容和组织的树状结构。此解析过程是增量的,这意味着浏览器可以在接收 HTML 时开始构建 DOM。重要的是,JavaScript 也可以与 DOM 交互并修改 DOM,从而导致页面结构的动态变化。浏览器还会将任何相关的 CSS 解析为 CSS 对象模型 (CSSOM)。与 DOM 构建不同,CSSOM 构建不是增量的;浏览器通常会阻止渲染,直到所有 CSS 都下载并处理完毕,因为 CSS 规则可以级联和覆盖,因此需要在渲染开始之前完全理解样式。
构建渲染树
一旦 DOM 和 CSSOM 都构建完成,浏览器就会继续构建渲染树。此树将 DOM 中的内容与 CSSOM 中的样式信息相结合,但它只包含页面上可见的元素。<head>
部分中的元素(通常)以及 display: none;
的元素都将从渲染树中排除。渲染树本质上表示将要绘制的文档的可视结构。
布局
有了渲染树之后,浏览器会执行布局,也称为重排 (reflow)。在此阶段,浏览器会计算渲染树中每个元素在视口内的精确位置和大小。布局是一个复杂且计算密集的过程,尤其是在处理大量 DOM 节点时,因为一个元素的尺寸和位置会影响其他元素。任何影响渲染树几何形状的更改,例如宽度、高度或位置的更改,都将触发布局。

绘制
布局之后,浏览器进入绘制阶段,在此阶段,它会遍历渲染树并为每个可视元素发出绘制指令。这包括用颜色填充像素、渲染文本、绘制图像、边框和阴影,通常在多个图层上进行。基于 Chromium 的浏览器使用 Skia 图形库来促进此绘制过程,这可能涉及 GPU 以实现硬件加速。重要的是,对于后续帧,只会重新绘制自上次绘制以来屏幕上发生变化的区域,从而优化了该过程。

合成
渲染管线的最后阶段是合成。在这里,浏览器将各种绘制的图层组合起来,以生成网页的最终视觉表示。此步骤会考虑元素的堆叠顺序 (z-index)、透明度和混合模式。合成也可以通过 GPU 进行硬件加速,从而显著提高性能,尤其是在涉及变换和不透明度的动画方面。

在 Chromium 中,渲染过程涉及一个多进程架构,包括浏览器进程、渲染器进程和 Viz 进程。渲染器进程负责特定选项卡的渲染管线,它包括一个合成器线程,该线程在处理滚动和动画方面起着至关重要的作用,通常将这些任务从主线程卸载以获得更好的性能。理解此管线对于理解 requestAnimationFrame
如何通过与浏览器的内部渲染机制协同工作来优化动画过程至关重要。
2 使用 setInterval
和 setTimeout
进行动画的性能限制
虽然 setInterval
和 setTimeout
提供了一种简单的方法来以特定的时间间隔或延迟执行 JavaScript 代码,但它们固有的特性在使用于动画时可能会导致严重的性能问题。这些限制源于它们缺乏对浏览器渲染管线的感知以及屏幕更新的时序。
javascript
function hello(name) {
console.log(`Hello, ${name}!`);
}
// 带参数的setTimeout,一秒后执行
setTimeout(hello, 1000, "World");
let count = 0;
const intervalId = setInterval(() => {
console.log(`Count: ${++count}`);
if (count >= 5) {
// setInterval 一定要有机制清除定时器放置内存泄漏
clearInterval(intervalId);
}
}, 1000);
问题一:时序不准确和漂移
setInterval
和 setTimeout
中指定的延迟表示回调函数执行之前的最短时间。如果主线程正忙于其他任务,例如 JavaScript 执行、样式计算或布局,则回调的实际执行可能会延迟。此外,setInterval
只是在指定的时间间隔之后将下一次执行排队,而不管上一次执行花费了多长时间。这可能会导致重叠执行,即在之前的回调完成之前触发新的回调。随着时间的推移,这些时序差异会累积,导致动画不同步,并且在不同的系统上显得卡顿或不一致。缺乏精确的计时意味着实现平滑、一致的帧速率(这对于流畅的动画至关重要)变得具有挑战性。
问题二:与浏览器渲染缺乏同步
setInterval
和 setTimeout
的运行独立于浏览器的渲染周期。它们可能会在次优的时间触发动画更新,从而可能导致不必要的重排和重绘。例如,更新可能发生在浏览器已经绘制完一帧之后,导致浪费了一个渲染周期,或者在浏览器准备好进行下一次重绘之前可能会触发多次更新。这种缺乏协调可能会导致资源利用效率低下,并导致动画卡顿。浏览器根据显示器的刷新率同步执行渲染,如果动画更新与此不同步,浏览器可能需要执行额外的工作,或者动画可能无法按预期显示。
问题二:选项卡在后台或最小化时也可能会继续执行
虽然一些现代浏览器对后台选项卡实施了节流以减少资源消耗,但依赖所有浏览器都具有此行为并不理想。在非活动选项卡中运行动画对用户没有任何视觉上的好处,并且会不必要地消耗 CPU 和电池资源。这可能会对整体系统性能和电池寿命产生负面影响,尤其是对于打开了多个选项卡的用户。
时序不准确、缺乏与渲染的同步以及在非活动选项卡中执行的潜在性相结合,导致使用 setInterval
和 setTimeout
创建的动画可能出现抖动和卡顿。流畅的动画需要与显示刷新率对齐的持续更新。这些计时器函数固有的局限性使得始终如一地实现这一点变得困难,从而导致动画显得断断续续或卡顿。当浏览器无法维持一致的帧速率时,就会发生卡顿,而 setInterval
和 setTimeout
的不可预测的时序会增加丢帧的可能性。
功能 | setInterval | setTimeout | requestAnimationFrame |
---|---|---|---|
计时精度 | 最短延迟,可能出现漂移和重叠 | 最短延迟,可能出现漂移 | 与浏览器重绘同步,高精度时间戳 |
与渲染同步 | 不了解浏览器渲染周期 | 不了解浏览器渲染周期 | 与浏览器重绘周期同步 |
资源使用(活动选项卡) | 可能导致不必要的重绘和重排 | 可能导致不必要的重绘和重排 | 优化渲染,减少不必要的操作 |
资源使用(非活动选项卡) | 可能继续执行,浪费资源 | 可能继续执行,浪费资源 | 暂停或节流,节省资源 |
卡顿的可能性 | 由于时序问题和缺乏同步而较高 | 由于时序问题和缺乏同步而较高 | 由于同步和优化而较低 |
用例 | 定期任务,轮询 | 单次延迟执行,一次性任务 | 动画,视觉更新 |
3 requestAnimationFrame
如何彻底改变动画计时
requestAnimationFrame
是一个浏览器 API,专门用于解决 setInterval
和 setTimeout
在创建平滑高效的 Web 动画方面的性能限制。它通过与浏览器渲染管线协调工作,提供了一种根本不同的方法。
回调机制
将一个回调函数作为参数提供给 requestAnimationFrame
,浏览器保证该函数将在下一次显示器重绘之前执行。重要的是要注意,requestAnimationFrame
是一种单次机制;如果需要后续的动画帧,则回调函数本身必须再次调用 requestAnimationFrame
。这种递归调用模式允许浏览器保持对动画时序的控制。这种回调机制允许浏览器精确控制动画更新发生的时间,使其与最佳渲染时间对齐。
javascript
let startTime = null;
function animate(currentTime) {
if (!startTime) startTime = currentTime;
const elapsed = currentTime - startTime;
const element = document.getElementById("myElement");
element.style.left = `${elapsed / 10}px`;
// 5秒后停止
if (elapsed < 5000) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
与浏览器渲染管线的同步
浏览器旨在在合成下一帧以显示之前立即调用回调函数。这种紧密的同步确保了动画更新在视觉上是一致的,并且与显示器的功能完美对齐。通过在重绘之前运行动画逻辑,requestAnimationFrame
最大限度地提高了更改在当前帧中可见的可能性,从而显著提高了动画的流畅度。

浏览器可以通过将使用 requestAnimationFrame
调度的多个并发动画的更新分组到单个重排和重绘周期中来优化它们。这减少了与多次渲染过程相关的开销。requestAnimationFrame
鼓励在同一帧内使用读-修改-写模式,有助于避免布局抖动 (layout thrashing),这是一种性能密集型场景,其中浏览器被迫在单个帧内多次重新计算布局。通过允许浏览器管理时序和批量渲染操作,requestAnimationFrame
可以提高整体性能并更有效地利用资源。
4 与显示刷新率同步
requestAnimationFrame
性能优势的一个基本方面在于它能够将动画更新与显示刷新率同步。刷新率通常为 60Hz,表示显示器大约每秒重绘其内容 60 次。requestAnimationFrame
的目标是将动画回调的执行与这些重绘对齐。
这通过最小化视觉撕裂和卡顿来确保流畅的视觉体验。浏览器尝试在每次显示器的垂直同步 (vSync) 事件之前调用 requestAnimationFrame
回调函数。vSync 是显示周期中显示器开始绘制下一帧的点。通过在此发生之前更新动画,浏览器确保更改在当前正在绘制到屏幕上的帧中可见,从而产生最自然和流畅的视觉输出。将动画更新与显示器的刷新周期对齐可以提供最自然和流畅的视觉输出。

requestAnimationFrame
也被设计为针对不同的刷新率进行优化。现代显示器通常具有更高的刷新率(例如,75Hz、90Hz、120Hz、144Hz)。如果系统性能允许,requestAnimationFrame
会适应设备的刷新率,在这些高刷新率显示器上更频繁地运行。这使得在功能强大的硬件上可以实现更流畅的动画。然而,为了确保动画以相同的预期速度运行,而不管每秒渲染的实际帧数如何,关键是使用 requestAnimationFrame
提供的时间戳来计算帧之间经过的时间,并相应地调整动画的进度。这种与设备无关的方法确保了在各种显示器上获得一致的视觉体验。
虽然标准建议与显示刷新率同步,但应该意识到不同浏览器在刷新率处理方面的差异。某些浏览器在特定情况下可能存在内部限制或帧速率上限。此外,在系统负载过重的情况下,某些浏览器可能会限制 requestAnimationFrame
回调,即使是在前台选项卡中。所以开发时应该在不同的浏览器和设备上测试他们的动画,以确保获得一致且最佳的用户体验。
5 避免不必要渲染优化性能
requestAnimationFrame
通过帮助避免不必要的重绘和重新计算,在优化性能方面发挥着至关重要的作用。通过安排动画更新在浏览器即将重绘屏幕之前发生,它确保了只有在实际需要时才进行渲染工作。
实现此目的的关键方法之一是最小化布局和绘制操作。当所有与动画相关的 DOM 操作都分组在 requestAnimationFrame
回调中时,浏览器可以有效地批量处理这些更改,并且对于所有更新,每帧只执行一次布局和绘制。这与 setInterval
或 setTimeout
形成鲜明对比,后者可能会在同一帧内触发多个单独的布局或绘制操作,导致冗余工作和性能瓶颈。减少这些昂贵的布局和绘制操作的频率对于提高整体性能至关重要,尤其是在具有复杂 DOM 结构和样式的网页上。
通过避免不必要的渲染以及暂停或限制非活动选项卡中的动画,requestAnimationFrame
显著有助于降低电池消耗。更低的 CPU 和 GPU 使用率直接转化为更少的功耗,这对于电池寿命有限的移动设备尤其重要。因此,高效的动画技术对于提供良好的用户体验而不过度消耗设备电池至关重要。
requestAnimationFrame
还鼓励使用仅合成器动画。当与已在合成图层上的元素的 CSS 变换和不透明度更改结合使用时,浏览器通常可以在合成器线程上主要处理这些动画,完全绕过主线程的布局和绘制阶段。这会带来更好的性能和更流畅的动画,因为合成器专门针对这些类型的视觉变化进行了优化。requestAnimationFrame
是触发这些仅合成器更新的理想机制,进一步减少了主线程的工作负载。
6 浏览器对 requestAnimationFrame
回调的优先级排序
浏览器对 requestAnimationFrame
回调进行优先级排序,以确保动画流畅运行并提供响应迅速的用户体验。这种优先级排序在这些回调在浏览器事件循环中的管理方式中显而易见。
requestAnimationFrame
回调在浏览器执行当前帧的布局和绘制之前执行。这种时序至关重要,因为它允许将动画更新包含在即将渲染的帧中。相比之下,使用 setTimeout
或 setInterval
调度的任务可能会在事件循环中的不同时间点执行,从而可能导致动画更新发生在帧已经绘制之后,从而导致视觉上的不一致。requestAnimationFrame
在事件循环中的特定时序确保了动画更新与渲染过程同步。

即使主线程正忙于其他任务,例如处理用户输入或执行复杂的 JavaScript,浏览器通常仍然优先处理 requestAnimationFrame
回调。这种优先级排序有助于保持动画的流畅性,这是感知性能的关键因素。浏览器可以通过暂时延迟不太重要的任务或优化执行顺序来确保及时处理动画更新。即使在适度的系统负载下,这种努力保持动画流畅性的做法也极大地提升了用户体验。

requestAnimationFrame
的时序还与其他旨在优化性能的浏览器 API 交互。例如,来自 ResizeObserver
(监视元素尺寸变化)和 IntersectionObserver
(跟踪元素的可见性)等 API 的回调可能会与 requestAnimationFrame
同步或在其后立即执行。这种协调管理浏览器中不同类型更新的方法体现了一种确保高效且响应迅速的 Web 应用程序的整体策略。
7 非活动选项卡中的智能资源管理
当选项卡或窗口不可见或不活动时,现代浏览器对 requestAnimationFrame
采用智能资源管理技术。这是一项至关重要的优化,可以显著提高性能和电池寿命。
暂停或限制回调机制
当浏览器选项卡移至后台或最小化时,大多数浏览器会自动暂停或显著限制 requestAnimationFrame
回调的执行。限制的程度因浏览器而异,有些浏览器将回调频率降低到每秒 1 帧甚至更低。这种智能行为可以防止不必要的 CPU 和 GPU 使用,从而显著节省电池寿命,尤其是在移动设备上,并在动画对用户不可见时提高整体系统性能。持续更新用户看不到的动画没有任何好处,因此暂停或限制这些更新可以为其他活动任务节省宝贵的系统资源。
自动恢复
当用户切换回选项卡并使其再次可见时,requestAnimationFrame
回调会无缝地在选项卡激活时恢复动画。浏览器会跟踪选项卡的可见性状态,并在隐藏的选项卡返回前台时自动恢复 requestAnimationFrame
的正常执行。在某些情况下,动画可能需要考虑选项卡处于非活动状态时经过的时间,以防止动画状态出现突然的跳跃或不一致。然而,浏览器通常会提供平滑的过渡,确保动画在用户返回选项卡后继续按预期运行。
8 跨浏览器引擎的底层实现
requestAnimationFrame
的底层实现是引擎特定的,但主要浏览器引擎(Blink(Chrome 和 Edge 使用)、Gecko(Firefox 使用)和 WebKit(Safari 使用))的基本原理保持一致。主要目标是与显示刷新率同步并优化性能。

Blink(Chrome 和 Edge)
在 Blink(Chrome 和 Edge 的引擎)中,渲染引擎在渲染器进程中运行,一个专用的合成器线程处理动画。Blink 努力以 vSync 间隔触发 requestAnimationFrame
动画,可能会利用 Windows 上的 D3DKMTWaitForVerticalBlankEvent
等操作系统级 API 来实现更精确的计时并减少抖动。这种与图形子系统的直接交互可以实现与显示器刷新周期的更紧密同步,从而实现更流畅的动画并降低 CPU 负载。
Gecko(Firefox)
Gecko(Firefox 的引擎)也具有高度优化的图形管线,专为高效的动画渲染而设计。Gecko 的目标是将 requestAnimationFrame
与显示器的刷新率同步。虽然具体的操作系统级 API 和内部机制可能与 Blink 不同,但 Gecko 的实现也优先考虑与显示的同步,以确保流畅的视觉输出。过去曾出现过 vSync 时间计算方面的差异,这突显了在不同的操作系统和硬件上实现一致计时的复杂性。
WebKit(Safari)
WebKit(Safari 使用)也实现了 requestAnimationFrame
以与浏览器的渲染周期同步。从历史上看,与 Blink 和 Gecko 相比,WebKit 在支持高刷新率方面可能存在限制。WebKit 正在不断发展其渲染引擎,包括统一 HTML 和 SVG 渲染管线的努力,这可能会对未来动画的处理方式产生影响。尽管存在一些历史差异,但 WebKit 仍然是通过 requestAnimationFrame
提供高性能动画功能的主要参与者。
浏览器引擎 | 核心浏览器 | 渲染进程/线程 | 主要实现细节 | 值得注意的行为/限制 |
---|---|---|---|---|
Blink | Chrome, Edge | 渲染器,合成器 | 旨在实现 vSync 同步,可能使用操作系统 API(例如,D3DKMTWaitForVerticalBlankEvent) | 支持 OffscreenCanvas requestAnimationFrame |
Gecko | Firefox | 图形管线 | 优先考虑 vSync 同步,将动画卸载到合成器 | 历史上 vSync 时间计算存在差异 |
WebKit | Safari | 渲染周期 | 与渲染周期同步 | 历史上可能在高刷新率支持方面存在限制 |
9 最后
与传统的 JavaScript 动画技术(如 setInterval
和 setTimeout
)相比,requestAnimationFrame
代表了显著的进步。其性能增强源于其基本设计原则:与显示刷新率同步 、通过避免不必要的布局和绘制操作来优化渲染 、降低 CPU 和 GPU 使用率 从而提高性能和电池寿命,以及在非活动选项卡中进行智能资源管理 。通过与浏览器的渲染管线协同工作,并利用引擎特定的底层优化,requestAnimationFrame
已成为现代 Web 动画开发的标准 API。强烈建议利用 requestAnimationFrame
来创建高性能、高效率且用户友好的 Web 体验,从而提供流畅的动画,而不会损害系统资源或电池寿命。
引用