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.xdnf.cn/news/9735.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【C++练习】使用海伦公式计算三角形面积

编写并调试一个计算三角形面积的程序 要求&#xff1a; 使用海伦公式&#xff08;Herons Formula&#xff09;来计算三角形的面积。程序需要从用户那里输入三角形的三边长&#xff08;实数类型&#xff09;。输出计算得到的三角形面积&#xff0c;结果保留默认精度。提示用户…

附件商户,用户签到,uv统计功能(geo,bitmap,hyperloglog结构的使用)

目录 附近商户一&#xff1a;Geo数据结构二&#xff1a;附近商户搜索 用户签到一&#xff1a;BitMap功能演示二&#xff1a;实现签到功能三&#xff1a;统计签到功能 uv统计一&#xff1a;hyperloglog的用法二&#xff1a;测试百万数据的tji二&#xff1a;测试百万数据的tji 附…

【LuatOS】修改LuatOS源码为PC模拟器添加高精度时间戳库timeplus

0x00 缘起 LuatOS以及Lua能够提供微秒或者毫秒的时间戳获取工具&#xff0c;但并没有提供获取纳秒的工具。通过编辑LuatOS源码以及相关BSP源码&#xff0c;添加能够获取纳秒的timeplus库并重新编译&#xff0c;以解决在64位Windows操作系统中LuatOS模拟器获取纳秒的问题&#…

[Python学习日记-64] 组合

[Python学习日记-64] 组合 简介 继承与组合 组合的使用 简介 继承其实就是生活当中的归类&#xff0c;就是把对象之间的共同特征再一次提炼&#xff0c;然后形成一个类&#xff0c;但是在实际的开发当中不单单只有归类这一个动作&#xff0c;对象与对象之间都会有一些关系&a…

关于stm32中IO映射的一些问题

在STM32固件库&#xff08;比如HAL或LL库&#xff09;中&#xff0c;GPIO的寄存器映射已经定义好了&#xff0c;开发者可以通过标准的读写操作访问GPIO引脚的状态。 一、我们可以直接通过位移操作来修改特定值。 二、下面我们提供另一种方法&#xff0c;位带操作 首先要定义一…

Python游戏开发之《人机大战象棋》-附完整源码-python教程

今天给大家带来的是人机大战的象棋 中国象棋 首先绘制一下棋盘&#xff0c;看看样子&#xff1a; 黑白经典款 绘制棋盘&#xff1a; class Board(QLabel):棋盘坐标与屏幕坐标类似&#xff0c;左上角为 (0, 0)&#xff0c;右下角为 (8, 9)BOARD str(dirpath / u"images…

AutoCAD2014

链接: https://pan.baidu.com/s/1Q4fhVmiSYDZ2DbPNi7m4cA 提取码: f3bm

免费送源码:Java+ssm+MySQL 在线购票影城 计算机毕业设计原创定制

摘要 随着互联网趋势的到来&#xff0c;各行各业都在考虑利用互联网将自己推广出去&#xff0c;最好方式就是建立自己的互联网系统&#xff0c;并对其进行维护和管理。在现实运用中&#xff0c;应用软件的工作规则和开发步骤&#xff0c;采用Java技术建设在线购票影城。 本设计…

MYSQL——事务管理

什么是事务 在数据库使用者角度&#xff0c;事务就是完成一个事件。例如一个员工信息数据库&#xff0c;要完成员工离职的事件&#xff0c;可能需要很多操作&#xff0c;比如删除员工基本信息以及员工在公司的表现&#xff0c;薪资水平等。而这一系列的操作就是为了完成员工离…

书生实战营第四期-基础岛第四关-InternLM + LlamaIndex RAG 实践

一、任务要求1 基于 LlamaIndex 构建自己的 RAG 知识库&#xff0c;寻找一个问题 A 在使用 LlamaIndex 之前 浦语 API 不会回答&#xff0c;借助 LlamaIndex 后 浦语 API 具备回答 A 的能力&#xff0c;截图保存。 1、配置开发机系统 镜像&#xff1a;使用 Cuda12.0-conda 镜…

LC:二分查找——杂记

文章目录 268. 丢失的数字162. 寻找峰值 268. 丢失的数字 LC将此题归类为二分查找&#xff0c;并且为简单题&#xff0c;下面记一下自己对这道题目的思考。 题目链接&#xff1a;268.丢失的数字 第一次看到这个题目&#xff0c;虽然标注的为简单&#xff0c;但肯定不能直接排…

推荐一款国产数据库管理工具Chat2DB

什么是 Chat2DB ? Chat2DB 是一款专为现代数据驱动型企业打造的数据库管理、数据开发及数据分析工具。作为一款AI原生的产品&#xff0c;Chat2DB 将人工智能技术与传统数据库管理功能深度融合&#xff0c;旨在提供更为智能、便捷的工作体验&#xff0c;助力用户高效地管理数据…

前端三件套(HTML + CSS + JS)

前言&#xff1a; 前端三件套&#xff0c;会用就行 毕竟在后面学习JavaWeb&#xff0c;以及在学习vue的时候也有帮助 前端三件套&#xff1a; HTML 定义网页的结构和内容。CSS 负责网页的样式和布局。JavaScript 添加动态交互和功能。 使用到的工具是Visual Studio Code 即…

Flutter错误: uses-sdk:minSdkVersion 16 cannot be smaller than version 21 declared

前言 今天要做蓝牙通信的功能&#xff0c;我使用了flutter_reactive_ble这个库&#xff0c;但是在运行的时候发现一下错误 Launching lib/main.dart on AQM AL10 in debug mode... /Users/macbook/Desktop/test/flutter/my_app/android/app/src/debug/AndroidManifest.xml Err…

网络编程示例之网络基础知识

TCP/IP 中有两个具有代表性的传输层协议&#xff0c;分别是 TCP 和 UDP&#xff1a; TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构&#xff0c;当应用程序采用 TCP 发送消息时&#xff0c;虽然可以保证发送的顺序&#xff0c;但还是犹如没有任何间隔的数据流发送…

十七:Spring Boot 依赖(2)-- spring-boot-starter-data-jpa 依赖详解

目录 1. 理解 JPA&#xff08;Java Persistence API&#xff09; 1.1 什么是 JPA&#xff1f; 1.2 JPA 与 Hibernate 的关系 1.3 JPA 的基本注解&#xff1a;Entity, Table, Id, GeneratedValue 1.4 JPA 与数据库表的映射 2. Spring Data JPA 概述 2.1 什么是 Spring Dat…

商品,订单业务流程梳理一

业务架构梳理 业务系统介绍 业务商品流程 业务订单流程 业务售后流程 系统架构 技术栈

HDR视频技术之二:光电转换与 HDR 图像显示

将自然界中的真实场景转换为屏幕上显示出来的图像&#xff0c;往往需要经过两个主要的步骤&#xff1a;第一个是通过摄影设备&#xff0c;将外界的光信息转换为图像信息存储起来&#xff0c;本质上是存储为数字信号&#xff1b;第二个是通过显示设备&#xff0c;将图像信息转换…

Linux完结

学习视频笔记均来自B站UP主" 泷羽sec",如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 【linux基础之病毒编写&#xff08;完结&#xff09;】 https://www.bilibili.com/video…

苹果iOS 18.4将允许欧盟地区的iPhone用户设置默认地图和翻译应用

在一份最新文件中&#xff0c;苹果概述了其为遵守欧盟数字市场法案所采取的措施&#xff0c;并透露将允许欧盟的 iPhone 和 iPad 用户从"2025 年春季"开始设置默认导航和翻译应用程序。 这一时间表表明&#xff0c;这些选项将在 iOS 18.4 和 iPadOS 18.4 中添加&…