动画其实是由一帧一帧的图像构成的。有 Web 动画那么就会存在该动画在播放运行时的帧率。而帧率在不同设备不同情况下又是不一样的。
有的时候,一些复杂SVGA或者CSS动画,我们需要实时监控它们的帧率,或者说是需要知道它们在不同设备的运行状况,从而更好的优化它们。
流畅的标准
首先,理清一些概念。FPS 表示的是每秒钟画面更新次数。我们平时所看到的连续画面都是由一幅幅静止画面组成的,每幅画面称为一帧,FPS 是描述“帧”变化速度的物理量。
理论上说,FPS 越高,动画会越流畅,目前大多数设备的屏幕刷新率为 60 次/秒,所以通常来讲 FPS 为 60 frame/s 时动画效果最好,也就是每帧的消耗时间为 (1000/60) 16.67ms。
当然,经常玩 FPS 游戏的朋友肯定知道,吃鸡/CSGO 等 FPS 游戏推荐使用 144HZ 刷新率的显示器,144Hz 显示器特指每秒的刷新率达到 144Hz 的显示器。相较于普通显示器每秒60的刷新速度,画面显示更加流畅。因此144Hz显示器比较适用于视角时常保持高速运动的第一人称射击游戏。
不过,这个只是显示器提供的高刷新率特性,对于我们 Web 动画而言,是否支持还要看浏览器,而大多数浏览器刷新率为 60 次/秒。
直观感受,不同帧率的体验:
- 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;
- 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;
- 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;
- 帧率波动很大的动画,亦会使人感觉到卡顿。
那么我们该如何通过JS来模拟获取我们页面动画当前的 FPS 值呢?
requestAnimationFrame
requestAnimationFrame 大家应该都不陌生,方法告诉浏览器您希望执行动画并请求浏览器调用指定的函数在下一次重绘之前更新动画。
- // 语法
- window.requestAnimationFrame(callback);
当你准备好更新屏幕画面时你就应用此方法。这会要求你的动画函数在浏览器下次重绘前执行。回调的次数常是每秒 60 次,大多数浏览器通常匹配 W3C 所建议的刷新率。
使用 requestAnimationFrame 计算 FPS 原理 原理是,正常而言 requestAnimationFrame 这个方法在一秒内会执行 60 次,也就是不掉帧的情况下。假设动画在时间 A 开始执行,在时间 B 结束,耗时 x ms。而中间 requestAnimationFrame 一共执行了 n 次,则此段动画的帧率大致为:n / (B - A)。
核心代码如下,能近似计算每秒页面帧率,以及我们额外记录一个 allFrameCount,用于记录 rAF 的执行次数,用于计算每次动画的帧率 :
- var rAF = function () {
- return (
- window.requestAnimationFrame ||
- window.webkitRequestAnimationFrame ||
- function (callback) {
- window.setTimeout(callback, 1000 / 60);
- }
- );
- }();
- var frame = 0;
- var allFrameCount = 0;
- var lastTime = Date.now();
- var lastFameTime = Date.now();
- var loop = function () {
- var now = Date.now();
- var fs = (now - lastFameTime);
- var fps = Math.round(1000 / fs);
- lastFameTime = now;
- // 不置 0,在动画的开头及结尾记录此值的差值算出 FPS
- allFrameCount++;
- frame++;
- if (now > 1000 + lastTime) {
- var fps = Math.round((frame * 1000) / (now - lastTime));
- console.log(`${new Date()} 1S内 FPS:`, fps);
- frame = 0;
- lastTime = now;
- };
- rAF(loop);
- }
- loop();
如果我们需要统计某个特定动画过程的帧率,只需要在动画开始和结尾两处分别记录 allFrameCount 这个数值大小,再除以中间消耗的时间,也可以得出特定动画过程的 FPS 值。
值得注意的是,这个方法计算的结果和真实的帧率肯定是存在误差的,因为它是将每两次主线程执行 javascript 的时间间隔当成一帧,而非上面说的主线程加合成线程所消耗的时间为一帧。但是对于现阶段而言,算是一种可取的方法。
适当美化一下
- import BUS from 'event-bus';
- // 计算性能指标
- (() => {
- const createConsole = (desc, val) => console.log(
- `%c${desc}`,
- 'color:#fff;background:red;padding:2px 6px;border-radius:3px;',
- val);
- window.addEventListener('load', () => {
- const timing = performance.timing;
- createConsole('DNS 解析耗时', timing.domainLookupEnd - timing.domainLookupStart);
- createConsole('TCP连接耗时', timing.connectEnd - timing.connectStart);
- createConsole('网络请求耗时', timing.responseStart - timing.requestStart);
- createConsole('数据传输耗时', timing.responseEnd - timing.requestStart);
- createConsole('页面首次渲染时间', timing.responseEnd - timing.navigationStart);
- createConsole('首次可交互时间', timing.domInteractive - timing.navigationStart);
- createConsole('DOM解析耗时', timing.domInteractive - timing.responseEnd);
- createConsole('DOM构建耗时', timing.domComplete - timing.domInteractive);
- createConsole('HTML 加载完成,DOM Ready', timing.domContentLoadedEventEnd - timing.navigationStart);
- createConsole('页面完全加载耗时', timing.loadEventStart - timing.navigationStart);
- });
- })();
- // FPS检测
- (() => {
- const limit = 3; // 出现低FPS的连续次数上限
- const below = 20; // 可容忍的最低FPS
- const updateInterval = 2 * 1000; // 检测帧率的间隔时间
- let updateTimer = 0; // 已经过去的时间
- let count = 0;
- let lastTime = performance.now();
- let frame = 0;
- let lastFameTime = performance.now();
- const loop = () => {
- frame += 1;
- const now = performance.now();
- const fs = (now - lastFameTime);
- lastFameTime = now;
- updateTimer += fs || 0;
- if (updateTimer < updateInterval) {
- window.requestAnimationFrame(loop);
- return;
- }
- updateTimer = 0;
- let fps = 0;
- fps = Math.round(1000 / fs);
- if (now > 1000 + lastTime) {
- fps = Math.round((frame * 1000) / (now - lastTime));
- frame = 0;
- lastTime = now;
- }
- if (fps < below) {
- count += 1;
- if (count >= limit) {
- console.log('网页卡顿', `连续${count}次FPS低于${below},当前FPS为${fps}`);
- BUS.trigger('fps-low'); // 关闭一些JS动画
- }
- } else {
- count = 0;
- }
- window.requestAnimationFrame(loop);
- };
- loop();
- })();
参考
Web 动画帧率(FPS)计算:https://www.cnblogs.com/coco1s/p/8029582.html