先有问题再有答案
浏览器帧率是多少?如何理解浏览器的一帧?浏览器的帧率和屏幕的刷新率是一回事嘛?有什么方式可以计算出浏览器的帧率嘛?帧率是固定的嘛?如果帧率不固定 我们在使用raf做一些功能时需要注意什么问题?
前置知识
浏览器的帧率
浏览器的帧率通常是指浏览器每秒能够绘制新帧的次数,通常为 60 帧每秒(60Hz)。这意味着浏览器每秒最多可以绘制 60 次新的图像。但是,实际的帧率可能会因设备性能、系统负载、浏览器设置等因素而有所不同。
手机刷新率 & 浏览器的刷新率
手机的刷新率和浏览器的刷新率是两个不同的概念,但它们都与屏幕的显示效果有关。
手机的刷新率,通常指的是屏幕硬件的刷新率,也就是屏幕每秒钟刷新图像的次数。一般来说,刷新率越高,屏幕显示的动画效果就越流畅。例如,一部180Hz刷新率的手机,意味着它的屏幕每秒钟可以刷新180次。
浏览器的刷新率,览器的刷新率,也被称为帧率,通常是由浏览器内核控制的, 指是每秒钟页面呈现的帧数,用于衡量页面的流畅程度
关于帧率与屏幕的关系可以参考这篇文章 js三座大山之异步五基于异步的js性能优化
浏览器的一帧

通常情况下,在一帧的时间内,浏览器会尽可能地执行JavaScript代码,浏览器会在一帧的结束时进行一次页面渲染。因为页面渲染(包括回流&重绘)是一个相对昂贵的操作,频繁地进行页面渲染会消耗大量的CPU和GPU资源,降低页面的性能。
我们可以理解为浏览器的一帧主要做三件事:
- 执行js代码,运行业务逻辑 浏览器:帧&事件循环
- 渲染页面UI 刷新屏幕 浏览器:帧&渲染流程
- 执行浏览器必要的回调事件 例如
requestAnimationFrame等函数。 浏览器: 深入理解requestAnimationFrame优化js运行时 所以我们说requestAnimationFrame是和帧率保持同步的。
背景问题
在使用requestAnimationFrame做一些位移动画时 因为在不同机型上requestAnimationFrame的执行频率是不同的,绝大部分是60hz 但是部分手机会执行120次/s 所以如果一个动画需要在X轴每帧移动0.5px总共需要移动120px的情况下, 在60hz的刷新率下耗时4s 在120hz的刷新率下耗时2s。速度快了一倍。这就是requestAnimationFrame做动画时会遇到的问题。
计算系统帧率
export const calculateAverageFps = (n = 5) => {
let _res: (value: any) => void;
const p = new Promise((resolve) => {
_res = resolve;
})
let frames = 0;
const startTime = performance.now();
function rafLoop() {
frames += 1;
if (performance.now() - startTime < n * 1000) {
requestAnimationFrame(rafLoop);
} else {
const elapsed = (performance.now() - startTime) / 1000;
const fps = Math.round(frames / elapsed);
_res(fps)
}
}
requestAnimationFrame(rafLoop);
return p;
}
// 调用函数,例如计算5秒的平均FPS
export const autoSysFps = calculateAverageFps(5);
calculateAverageFps 函数通过 requestAnimationFrame 计数在指定时间段内的帧数,并计算平均帧率。这里默认值为默认值为 5 秒,它返回一个 Promise,可以在 then 方法中获取计算结果。
这个函数可以帮助开发者了解当前环境下的实际帧率,从而优化动画和其他高频率任务的性能。
fpsLimiter
fpsLimiter 函数提供了一套灵活的帧率管理工具,可以帮助开发者在不同设备和不同帧率环境下,确保动画和其他高频率任务的性能和一致性。
fpsLimiter 函数返回一个对象,该对象包含以下三个方法:
- startExpectFps(cb: ICallBack, fps: number): 以指定的帧率执行回调。
- startSysFps(cb: ICallBack): 以系统帧率执行回调。
- raf(cb: ICallBack): 根据系统帧率和预期帧率选择合适的执行方式。
通过 startExpectFps、startSysFps 和 raf 方法,开发者可以选择合适的帧率管理策略,确保应用在各种条件下的表现。
github
完整代码参考: run-time-opti.fps
startExpectFps方法:
以指定的帧率执行回调函数。
参数:
- cb: ICallBack:回调函数,类型为 (currentFps?: number) => void。
- fps: number:期望的帧率。
const startExpectFps = (cb: ICallBack, fps: number) => {
let stop = false;
let rafId = 0;
let frameCount = 0;
const fpsInterval = 1000 / fps;
let now = 0;
let then = Date.now();
const startTime = then;
let delta;
let currentFps;
function loop() {
if (stop) {
return;
}
rafId = requestAnimationFrame(loop);
now = Date.now();
delta = now - then;
if (delta > fpsInterval) {
then = now - (delta % fpsInterval);
if (cb) {
const sinceStart = now - startTime;
currentFps = Math.round((1000 / (sinceStart / ++frameCount)) * 100) / 100;
cb(currentFps);
}
}
}
const stopFps = () => {
stop = true;
if (rafId) {
cancelAnimationFrame(rafId);
}
};
loop();
return stopFps
};
以15fps的频率运行这个函数。
const stop = startExpectFps(()=>{console.log('test')}, 15)
stop() // 停止循环
startSysFps 方法:
以系统帧率执行回调函数。功能等价于requestAnimationFrame. 区别是可以通过返回的回调取消循环。
const stop = startSysFps(()=>{console.log('test')}) // 以系统帧率运行回调函数
stop(); // 停止运行
raf 方法:
根据系统帧率和预期帧率选择合适的执行方式。如果系统帧率大于预期帧率(默认为 60),则以预期帧率执行回调;否则,以系统帧率执行回调。
使用 autoSysFps 异步获取系统帧率。
- 如果系统帧率大于预期帧率,则调用 startExpectFps 以预期帧率执行回调。
- 否则,调用 startSysFps 以系统帧率执行回调。
/***
* raf控制requestAnimationFrame执行频率 = Min(系统, 60)
* 为了兼容部分帧率为120fps的系统
*/
const raf = async (cb: ICallBack) => {
const expFps = 60;
return autoSysFps.then((sysFps: any) => {
if (sysFps > expFps) {
return startExpectFps(cb, expFps)
} else {
return startSysFps(cb)
}
})
}
demo
<script setup>
import { ref } from 'vue';
const { startExpectFps, startSysFps, raf } = fpsLimiter();
const content = ref(null);
const totalDuration = 2000; // 动画总时长为 1 秒
const totalDistance = 200; // 总移动距离为 100px
let startTime = null;
let stopRaf = null;
function animate() {
console.log('test animate');
const currentTime = Date.now();
if (!startTime) {
startTime = currentTime;
}
const elapsedTime = currentTime - startTime;
if (elapsedTime < totalDuration) {
const progress = elapsedTime / totalDuration;
const tx = progress * totalDistance;
content.value.style.transform = `translateX(${tx}px)`;
} else {
// 动画结束,设置最终位置
content.value.style.transform = `translateX(${totalDistance}px)`;
stopRaf?.();
}
}
const startAnimation = () => {
startTime = null; // 重置开始时间
raf(animate).then((callback) => {
console.log('test callback', callback);
stopRaf?.();
stopRaf = callback;
});
};
</script>
<template>
<div ref="content" class="content"></div>
<button @click="startAnimation">开始动画</button>
</template>
<style scoped>
.content {
width: 100px;
height: 100px;
background-color: lightblue;
}
</style>
stopRaf = startExpectFps(animate, 30); 希望帧率30
stopRaf = startSysFps(animate); 帧率跟随系统 这里浏览器为60/s
raf(animate).then((callback) => {
console.log('test callback', callback);
stopRaf?.();
stopRaf = callback;
});
这里是希望帧率保持60/s 的频率运行。 返回的回调可以停止运行
相关文章
浏览器:帧&事件循环
浏览器:帧&渲染流程
浏览器: 深入理解requestAnimationFrame优化js运行时
js三座大山之异步五基于异步的js性能优化