fpsLimiter: js实现帧率管理工具

1,385 阅读6分钟

先有问题再有答案

  1. 浏览器帧率是多少?
  2. 如何理解浏览器的一帧?
  3. 浏览器的帧率和屏幕的刷新率是一回事嘛?
  4. 有什么方式可以计算出浏览器的帧率嘛?
  5. 帧率是固定的嘛?
  6. 如果帧率不固定 我们在使用raf做一些功能时需要注意什么问题?

前置知识

浏览器的帧率

浏览器的帧率通常是指浏览器每秒能够绘制新帧的次数,通常为 60 帧每秒(60Hz)。这意味着浏览器每秒最多可以绘制 60 次新的图像。但是,实际的帧率可能会因设备性能、系统负载、浏览器设置等因素而有所不同。

手机刷新率 & 浏览器的刷新率

手机的刷新率和浏览器的刷新率是两个不同的概念,但它们都与屏幕的显示效果有关。

手机的刷新率,通常指的是屏幕硬件的刷新率,也就是屏幕每秒钟刷新图像的次数。一般来说,刷新率越高,屏幕显示的动画效果就越流畅。例如,一部180Hz刷新率的手机,意味着它的屏幕每秒钟可以刷新180次。

浏览器的刷新率,览器的刷新率,也被称为帧率,通常是由浏览器内核控制的, 指是每秒钟页面呈现的帧数,用于衡量页面的流畅程度

关于帧率与屏幕的关系可以参考这篇文章 js三座大山之异步五基于异步的js性能优化

浏览器的一帧

截屏2024-04-07 下午8.07.00.png

通常情况下,在一帧的时间内,浏览器会尽可能地执行JavaScript代码,浏览器会在一帧的结束时进行一次页面渲染。因为页面渲染(包括回流&重绘)是一个相对昂贵的操作,频繁地进行页面渲染会消耗大量的CPU和GPU资源,降低页面的性能。

我们可以理解为浏览器的一帧主要做三件事:

  1. 执行js代码,运行业务逻辑 浏览器:帧&事件循环
  2. 渲染页面UI 刷新屏幕 浏览器:帧&渲染流程
  3. 执行浏览器必要的回调事件 例如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

30fps.gif

stopRaf = startSysFps(animate); 帧率跟随系统 这里浏览器为60/s

fps60.gif

  raf(animate).then((callback) => {
        console.log('test callback', callback);
        stopRaf?.();
        stopRaf = callback;
    });

这里是希望帧率保持60/s 的频率运行。 返回的回调可以停止运行

相关文章

浏览器:帧&事件循环
浏览器:帧&渲染流程
浏览器: 深入理解requestAnimationFrame优化js运行时
js三座大山之异步五基于异步的js性能优化