requestAnimationFrame与setInterval的抉择

🙌 如文章有误,恳请评论区指正,谢谢!
❤ 写作不易,「点赞」+「收藏」+「转发」 谢谢支持!

背景

在之前的业务中遇到有 JS 动画的实现场景,但当电脑打开太多网页或是同时启动很多应用时,JS 动画便开始出现“掉帧”的现象,此篇文章便是探讨用 setIntervalrequestAnimationFrame 实现动画的差异性究竟在哪?为什么普遍都说 requestAnimationFrame 更优?

当然,如果业务允许,我们还可以通过 canvas 动画或是帧动画来重新实现该 JS 动画,只不过此篇文章重在探讨 JS 动画

结论打头

先说选择场景,如果是要实现对时间精度要求高的业务,例如动画或是其他每一帧都要实现数据更新的情况,建议选择 requestAnimationFrame;如果只是设置定时器,每隔一段时间就执行某件任务(对精度要求不高),例如实现数据上报等等,可以选择 setInterval

setInterval 的局限性

1. 时间精度

其实如果单从性能的角度考虑(即对内存占有率),setIntervalrequestAnimationFrame 其实是没差别的基本,所以两个 API 主要是在时间精度上有区别。

因为 setInterval 是宏任务,所以在浏览器事件循环 Event Loop 中优先级很低,得等前面的微任务队列清空以及浏览器完成渲染更新后,才执行宏任务。

因此,当执行 setInterval 时如果业务同时还需要大量的其他计算,例如 JS 对后端返回的数据进行分类或是 filter,或是有复杂表单需要进行渲染更新时,setInterval 就无法按预期的时间执行,比如原本是每 50ms 执行一次,现在就是 80ms。

2. 内存占用

同时,浏览器还能够做到自动暂停在后台选项卡或隐藏<iframe>setInterval 中的 requestAnimationFrame。而对于 setInterval,如果你不去 clearInterval,它就会不断执行,即一直占着部分内存,这也是我们常说的内存泄漏的一种场景(没及时清除计时器)。



requestAnimationFrame 的优点

先看一下来自 MDN - requestAnimationFrame 的介绍:

MDN - requestAnimationFrame

requestAnimationFrame 可以保证浏览器的每一帧都执行你传入的 callback 函数,也就是对于 60HZ 刷新率的屏幕来说,通过递归调用 requestAnimationFrame,可以实现在 1s(1000ms) 内执行 60 次。通过这个性质,我们可以在 callback 中执行动画操作,以此来实现动画的流畅运行(不掉帧)。

requestAnimationFrame 存在的问题

1. 问题来源(理论上)

虽然该 API 可以保证每一帧都执行 callback,但如果使用不恰当也因此而有动画精度的问题。

例如我要实现 2s(2000ms) 内将元素A 往右移 200px,如果不作任何处理,直接在传入的 callback 中每次执行都往右移动 xxx px,直到到达 200px 才停止递归。听起来实现很简单,甚至在自己的显示器中自测也是发现不了问题。

但对于 144HZ 和 60HZ 刷新率的显示器来说,requestAnimationFrame 执行的频率是不同的,空闲状态下 144HZ 会执行 144次,60HZ 会执行 60次,即在 144HZ 下动画会更快地执行,完成右移的时间会更短。因此其实是不符合用户体验预期的,更快执行让用户会有眼花缭乱的感觉。

2. 实际上呢?

但实际上并不是刷新率越高,requestAnimationFrame 就会执行越频繁,是个概率事件,有时会同步到 144HZ 左右的刷新率,有时仍锁定在 60HZ 左右的刷新率,该 bug 可查看该文章:考古挖掘:高刷显示器下的 rAF。

即的确在一部分用户下,刷新率和 requestAnimationFrame 存在不同步问题。可是在回答区,有一部分用户也反馈,他们屏幕刷新率和 requestAnimationFrame同步的。这样也印证了大概率是一个Bug。

如何解决 requestAnimationFrame 的频率不稳定问题

1. 根据不同刷新率屏幕,配置不同的动画速率

我们可以先封装一个 Hook,用于检测当前屏幕的刷新频率,大体代码如下:

  const count = useRef(0);const rAF = useRef();useEffect(() => {const prevTime = new Date();const animate = () => {console.log(count.current);count.current = count.current + 1;if (count.current >= 60) {// 就不继续递归了const nowTime = new Date();console.log("此时时间差值是:", nowTime - prevTime);console.log("此时的帧率是:", 60 / (nowTime - prevTime));rAF.current && cancelAnimationFrame(rAF.current);} else {requestAnimationFrame(animate)}}rAF.current = requestAnimationFrame(animate);rAF.current();return () => {rAF.current && cancelAnimationFrame(rAF.current);}}, [])

便有了对比,当我们实现的动画是在 60HZ 刷新率的屏幕下时每 100ms 移动 A px,那么当来到 144HZ 的屏幕时,便是每 100ms 移动 60/144 * A px 即可

60HZ 屏幕下
60HZ 屏幕下

144HZ 屏幕下
144HZ 屏幕下

上图是我在单显示器下时,测试我的两块显示器屏幕的测试结果,可见确实并不一定 144HZ 就一定更快,所以封装该 Hook 可以根据不同刷新率显示器来制定更精确的动画移动步频

2. 根据时间差值来百分比地移动

这也是 MDN - requestAnimationFrame 的官方介绍用法,requestAnimationFrame 的 callback 回调函数会有一个传入参数,这个参数是一个 long 类型整数值,是在回调列表里的唯一标识符,用于表示上一帧渲染的结束时间。关键代码片段和解释注释如下:

const element = document.getElementById("some-element-you-want-to-animate");
let start, previousTimeStamp;
let done = false;function step(timestamp) {// 第一次就先初始化起始时间,后面就不断更新最新时间来跟这个起始时间 start 作差值,来对比// 等价于第 0 帧的时间戳if (start === undefined) {start = timestamp;}const elapsed = timestamp - start; // 时间差值if (previousTimeStamp !== timestamp) {// 这里使用 Math.min() 确保元素在恰好位于 200px 时停止运动const count = Math.min(0.1 * elapsed, 200);element.style.transform = `translateX(${count}px)`;if (count === 200) done = true;}if (elapsed < 2000) {// 2 秒之后停止动画previousTimeStamp = timestamp;if (!done) {window.requestAnimationFrame(step);}}
}window.requestAnimationFrame(step);

在这个例子中,一个元素的动画时间是 2 秒(2000 毫秒)。该元素以 0.1px/ms 的速度向右移动,所以它的相对位置(以 CSS 像素为单位)可以通过动画开始后所经过的时间(以毫秒为单位)的函数来计算,即 0.1 * elapsed。该元素的最终位置是在其初始位置的右边 200px(0.1 * 2000)。

当然,代码中的 0.1 是我们可以改变的,比如你是想 1s 旋转 360度,即 360°/1000ms -> 0.36°/ms,那就代码改成

const count = Math.min(0.36 * elapsed, 360);
element.style.transform = `rotate(${count}deg)`;

便可实现按时间差值来百分比执行动画的需求了。




时间差值的计算还可以如下计算,该实现就可以不利用传入参数,而是每次都调 performance.now() 来获取最新时间:

const zero = performance.now();
requestAnimationFrame(animate);
function animate() {const value = (performance.now() - zero) / duration;if (value < 1) {element.style.opacity = value;requestAnimationFrame((t) => animate(t));} else element.style.opacity = 1;
}

第二种方案与 setInterval 的不同之处

其实第二种方案也是类似于 setInterval,因为是按时间差值来执行动画,但通过 requestAnimationFrame 你可以保证动画函数执行的优先级,在保证了每帧必定调用的前提下,再去精确计算该如何执行动画。

当然,如果对 CSS 动画性能比较感兴趣的,也可以查看该 MDN - CSS 动画与 JavaScript 动画的性能 文档进行对比阅读。



最后

我是 Smoothzjc,致力于产出更多且不仅限于前端方面的优质文章

大家也可以关注我的公众号 @ Smooth前端成长记录,及时通过移动端获取到最新文章消息!

写作不易,「点赞」+「收藏」+「转发」 谢谢支持❤

往期推荐

《拒绝死记硬背!清晰思路讲透 控制并发数、Promise.all、Promise.race 的实现逻辑》

《手把手教前端从0到1通过 Node + Express 开发简易接口,项目开发+部署服务器(亲身痛苦经历)》

《都2022年了还不考虑来学React Hook吗?6k字带你从入门到吃透》

《一份不可多得的 Webpack 学习指南(1万字长文带你入门 Webpack 并掌握常用的进阶配置)》

《通过 React15 ~ 17 的优化迭代来简单聊聊 Fiber》

《【offer 收割机之面试必备】一篇非常全面的 从 URL 输入到页面展现的全过程 精华梳理》

《【offer 收割机之手写系列】10分钟带你掌握原理并手写防抖与节流的立即/非立即执行版本》

《【offer 收割机之 CSS 回顾系列】请你解释一下什么是 BFC ?他的应用场景有哪些?》

《Github + hexo 实现自己的个人博客、配置主题(超详细)》

《10分钟让你彻底理解如何配置子域名来部署多个项目》

《一文理解配置伪静态解决 部署项目刷新页面404问题

《带你3分钟掌握常见的水平垂直居中面试题》

《【建议收藏】长达万字的git常用指令总结!!!适合小白及在工作中想要对git基本指令有所了解的人群》

《浅谈javascript的原型和原型链(新手懵懂想学会原型链?看这篇文章就足够啦!!!)》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/468399.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

高性能分布式缓存Redis-分布式锁与布隆过滤器

一、分布式锁 我们先来看一下本地锁 在并发编程中&#xff0c;我们通过锁&#xff0c;来避免由于竞争而造成的数据不一致问题。通常&#xff0c;我们以 synchronized 、Lock 来使用它&#xff08;单机情况&#xff09; 来看这段代码 Autowired RedisTemplate<String,Str…

Flutter运行App时出现“Running Gradle task ‘assembleDebug“问题解决

在参考了众多解决办法中最有用并且最快的方法 Gradle Distributions 在这个地方下载对应你这个文件中的gradle版本 然后将 最后一行本来不是这样的,我们把下载好的zip包保存到本地,然后用这个代替网址,最后成功运行

【CUDA】认识CUDA

目录 一、CUDA编程 二、第一个CUDA程序 三、CUDA关键字 四、device管理 4.1 初始化 4.2 Runtime API查询GPU信息 4.3 决定最佳GPU CUDA C 编程指南CUDA C在线文档&#xff1a;CUDA C 编程指南 CUDA是并行计算的平台和类C编程模型&#xff0c;能很容易的实现并行算法。只…

【优选算法篇】微位至简,数之恢宏——解构 C++ 位运算中的理与美

文章目录 C 位运算详解&#xff1a;基础题解与思维分析前言第一章&#xff1a;位运算基础应用1.1 判断字符是否唯一&#xff08;easy&#xff09;解法&#xff08;位图的思想&#xff09;C 代码实现易错点提示时间复杂度和空间复杂度 1.2 丢失的数字&#xff08;easy&#xff0…

从0开始学习机器学习--Day21--算法的评估标准

准确率和召回率(precision and recall) 在上一章我们提到了在每次运行算法时通过返回一个实数值来判断算法的好坏&#xff0c;但是我们该如何构建这个实数的计算公式呢&#xff0c;毕竟这关乎于我们对算法的判断&#xff0c;不能过于夸大或贬低。有一个典型的会被影响的很大例…

自然语言处理在客户服务中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 自然语言处理在客户服务中的应用 自然语言处理在客户服务中的应用 自然语言处理在客户服务中的应用 引言 自然语言处理概述 定义…

【Ubuntu24.04】从双系统到虚拟机再到单系统的故事

故事 在大学前期&#xff0c;我使用Ubuntu系统都是为了学习一些命令或者其它Linux的东西&#xff0c;对性能的要求不高&#xff0c;所以选择了虚拟机&#xff0c;后来为了做毕设&#xff0c;选择安装了Ubuntu20.04双系统&#xff0c;因为虚拟机实在带不动&#xff0c;那时我的主…

初次体验Tauri和Sycamore(1)

原创作者&#xff1a;庄晓立&#xff08;LIIGO&#xff09; 原创时间&#xff1a;2024年11月10日 原创链接&#xff1a;https://blog.csdn.net/liigo/article/details/143666827 版权所有&#xff0c;转载请注明出处。 前言 Tauri 2.0发布于2024年10月2日&#xff0c;Sycamore…

【统计子矩阵——部分前缀和+双指针】

题目 代码 #include <bits/stdc.h> using namespace std; typedef long long ll; const int N 510; int s[N][N]; int main() {ios::sync_with_stdio(0);cin.tie(0);int n, m, k;cin >> n >> m >> k;for(int i 1; i < n; i)for(int j 1; j <…

「QT」QT5程序设计专栏目录

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「QT」QT5程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasolid…

Qt学习笔记第41到50讲

第41讲 UI美化遗留问题解决 如上图所示目前记事本的雏形已现&#xff0c;但是还是有待优化&#xff0c;比如右下角的拖动问题。 解决方法&#xff1a; ①首先修改了Widget类的构造函数。 Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) {ui->s…

深度学习经典模型之ZFNet

1 ZFNet 1.1 模型介绍 ​ ZFNet是由 M a t t h e w Matthew Matthew D . Z e i l e r D. Zeiler D.Zeiler和 R o b Rob Rob F e r g u s Fergus Fergus在AlexNet基础上提出的大型卷积网络&#xff0c;在2013年ILSVRC图像分类竞赛中以11.19%的错误率获得冠军&#xff08;实际…

移动应用开发:简易登录页

文章目录 简介一&#xff0c;创建新活动二&#xff0c;设计UI布局三&#xff0c;编写活动代码四&#xff0c;运行应用程序注意 简介 使用Android Studio编写的简单Android 登录应用程序&#xff0c;该应用程序包含一个登录界面&#xff0c;具有账号和密码两个文本框&#xff0…

网络基础:http协议和内外网划分

声明 学习视频来自B站UP主泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 泷羽sec的个人空间-泷羽sec个人主页-哔哩哔哩视频https://space.bilibili.com/350329294 一&#xff0c;H…

英飞凌Aurix2G TC3XX GPT12模块详解

英飞凌Aurix2G TC3XX GPT12模块详解 本文主要介绍英飞凌 Aurix2G TC3XX系列芯片GPT12模块硬件原理、MCAL相关配置和部分代码实现。 文章目录 英飞凌Aurix2G TC3XX GPT12模块详解1 模块介绍2 功能介绍2.1 结构2.2 独立运行模式2.2.1 定时器模式2.2.2 门控定时器模式2.2.3 计数…

大数据程序猿不可不看的资料大全

​ 随着大数据技术的发展&#xff0c;大数据程序猿在数据采集、处理、分析、存储等方面的技能需求不断增加。要在这个领域保持竞争力&#xff0c;系统性地学习和掌握大数据工具、技术架构和行业趋势是非常重要的。以下为您提供一份围绕大数据程序猿不可不看的资料大全&#xf…

抓包工具WireShark使用记录

目录 网卡选择&#xff1a; 抓包流程&#xff1a; 捕获过滤器 常用捕获过滤器&#xff1a; 抓包数据的显示 显示过滤器&#xff1a; 常用的显示过滤器&#xff1a; 实际工作中&#xff0c;在平台对接&#xff0c;设备对接等常常需要调试接口&#xff0c;PostMan虽然可以进…

MySQL数据迁移到SQLServer数据库

随着云计算技术的发展以及大数据时代的到来&#xff0c;越来越多的企业开始寻求更加高效、安全的数据管理解决方案。MySQL作为一种开源的关系型数据库管理系统&#xff0c;在互联网应用开发中占据了极其重要的位置&#xff1b;而另一方面&#xff0c;Microsoft SQL Server凭借其…

【STM32开发】-FreeRTOS开发入手学习

一、什么是FreeRTOS&#xff1f; FreeRTOS 是 RTOS 系统的一种&#xff0c;FreeRTOS 十分的小巧&#xff0c;可以在资源有限的微控制器中运行&#xff1b; 1、 FreeRTOS是免费的。 2、许多其他半导体厂商产品的 SDK 包就使用 FreeRTOS 作为其操作系统&#xff0c;尤其是 WIFI、…

【软考】系统分析师第二版 新增章节 第20章微服务系统分析与设计

微服务系统是一类基于微服务架构风格的分布式系统&#xff0c;它将应用程序拆分成多个独立的小型服务&#xff0c;每个服务都运行在独立的进程中&#xff0c;并采用轻量级通信协议进行通信。这些服务可以由不同的团队开发、不同的编程语言编写&#xff0c;并且可以按需部署。微…