JS模擬監(jiān)控頁面FPS幀率
動(dòng)畫其實(shí)是由一幀一幀的圖像構(gòu)成的。有 Web 動(dòng)畫那么就會(huì)存在該動(dòng)畫在播放運(yùn)行時(shí)的幀率。而幀率在不同設(shè)備不同情況下又是不一樣的。
有的時(shí)候,一些復(fù)雜SVGA或者CSS動(dòng)畫,我們需要實(shí)時(shí)監(jiān)控它們的幀率,或者說是需要知道它們在不同設(shè)備的運(yùn)行狀況,從而更好的優(yōu)化它們。
流暢的標(biāo)準(zhǔn)
首先,理清一些概念。FPS 表示的是每秒鐘畫面更新次數(shù)。我們平時(shí)所看到的連續(xù)畫面都是由一幅幅靜止畫面組成的,每幅畫面稱為一幀,F(xiàn)PS 是描述“幀”變化速度的物理量。
理論上說,F(xiàn)PS 越高,動(dòng)畫會(huì)越流暢,目前大多數(shù)設(shè)備的屏幕刷新率為 60 次/秒,所以通常來講 FPS 為 60 frame/s 時(shí)動(dòng)畫效果最好,也就是每幀的消耗時(shí)間為 (1000/60) 16.67ms。
當(dāng)然,經(jīng)常玩 FPS 游戲的朋友肯定知道,吃雞/CSGO 等 FPS 游戲推薦使用 144HZ 刷新率的顯示器,144Hz 顯示器特指每秒的刷新率達(dá)到 144Hz 的顯示器。相較于普通顯示器每秒60的刷新速度,畫面顯示更加流暢。因此144Hz顯示器比較適用于視角時(shí)常保持高速運(yùn)動(dòng)的第一人稱射擊游戲。
不過,這個(gè)只是顯示器提供的高刷新率特性,對于我們 Web 動(dòng)畫而言,是否支持還要看瀏覽器,而大多數(shù)瀏覽器刷新率為 60 次/秒。
直觀感受,不同幀率的體驗(yàn):
- 幀率能夠達(dá)到 50 ~ 60 FPS 的動(dòng)畫將會(huì)相當(dāng)流暢,讓人倍感舒適;
- 幀率在 30 ~ 50 FPS 之間的動(dòng)畫,因各人敏感程度不同,舒適度因人而異;
- 幀率在 30 FPS 以下的動(dòng)畫,讓人感覺到明顯的卡頓和不適感;
- 幀率波動(dòng)很大的動(dòng)畫,亦會(huì)使人感覺到卡頓。
那么我們該如何通過JS來模擬獲取我們頁面動(dòng)畫當(dāng)前的 FPS 值呢?
requestAnimationFrame
requestAnimationFrame 大家應(yīng)該都不陌生,方法告訴瀏覽器您希望執(zhí)行動(dòng)畫并請求瀏覽器調(diào)用指定的函數(shù)在下一次重繪之前更新動(dòng)畫。
- // 語法
- window.requestAnimationFrame(callback);
當(dāng)你準(zhǔn)備好更新屏幕畫面時(shí)你就應(yīng)用此方法。這會(huì)要求你的動(dòng)畫函數(shù)在瀏覽器下次重繪前執(zhí)行。回調(diào)的次數(shù)常是每秒 60 次,大多數(shù)瀏覽器通常匹配 W3C 所建議的刷新率。
使用 requestAnimationFrame 計(jì)算 FPS 原理 原理是,正常而言 requestAnimationFrame 這個(gè)方法在一秒內(nèi)會(huì)執(zhí)行 60 次,也就是不掉幀的情況下。假設(shè)動(dòng)畫在時(shí)間 A 開始執(zhí)行,在時(shí)間 B 結(jié)束,耗時(shí) x ms。而中間 requestAnimationFrame 一共執(zhí)行了 n 次,則此段動(dòng)畫的幀率大致為:n / (B - A)。
核心代碼如下,能近似計(jì)算每秒頁面幀率,以及我們額外記錄一個(gè) allFrameCount,用于記錄 rAF 的執(zhí)行次數(shù),用于計(jì)算每次動(dòng)畫的幀率 :
- 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,在動(dòng)畫的開頭及結(jié)尾記錄此值的差值算出 FPS
- allFrameCount++;
- frame++;
- if (now > 1000 + lastTime) {
- var fps = Math.round((frame * 1000) / (now - lastTime));
- console.log(`${new Date()} 1S內(nèi) FPS:`, fps);
- frame = 0;
- lastTime = now;
- };
- rAF(loop);
- }
- loop();
如果我們需要統(tǒng)計(jì)某個(gè)特定動(dòng)畫過程的幀率,只需要在動(dòng)畫開始和結(jié)尾兩處分別記錄 allFrameCount 這個(gè)數(shù)值大小,再除以中間消耗的時(shí)間,也可以得出特定動(dòng)畫過程的 FPS 值。
值得注意的是,這個(gè)方法計(jì)算的結(jié)果和真實(shí)的幀率肯定是存在誤差的,因?yàn)樗菍⒚績纱沃骶€程執(zhí)行 javascript 的時(shí)間間隔當(dāng)成一幀,而非上面說的主線程加合成線程所消耗的時(shí)間為一幀。但是對于現(xiàn)階段而言,算是一種可取的方法。
適當(dāng)美化一下
- import BUS from 'event-bus';
- // 計(jì)算性能指標(biāo)
- (() => {
- 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 解析耗時(shí)', timing.domainLookupEnd - timing.domainLookupStart);
- createConsole('TCP連接耗時(shí)', timing.connectEnd - timing.connectStart);
- createConsole('網(wǎng)絡(luò)請求耗時(shí)', timing.responseStart - timing.requestStart);
- createConsole('數(shù)據(jù)傳輸耗時(shí)', timing.responseEnd - timing.requestStart);
- createConsole('頁面首次渲染時(shí)間', timing.responseEnd - timing.navigationStart);
- createConsole('首次可交互時(shí)間', timing.domInteractive - timing.navigationStart);
- createConsole('DOM解析耗時(shí)', timing.domInteractive - timing.responseEnd);
- createConsole('DOM構(gòu)建耗時(shí)', timing.domComplete - timing.domInteractive);
- createConsole('HTML 加載完成,DOM Ready', timing.domContentLoadedEventEnd - timing.navigationStart);
- createConsole('頁面完全加載耗時(shí)', timing.loadEventStart - timing.navigationStart);
- });
- })();
- // FPS檢測
- (() => {
- const limit = 3; // 出現(xiàn)低FPS的連續(xù)次數(shù)上限
- const below = 20; // 可容忍的最低FPS
- const updateInterval = 2 * 1000; // 檢測幀率的間隔時(shí)間
- let updateTimer = 0; // 已經(jīng)過去的時(shí)間
- 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('網(wǎng)頁卡頓', `連續(xù)${count}次FPS低于${below},當(dāng)前FPS為${fps}`);
- BUS.trigger('fps-low'); // 關(guān)閉一些JS動(dòng)畫
- }
- } else {
- count = 0;
- }
- window.requestAnimationFrame(loop);
- };
- loop();
- })();
參考
Web 動(dòng)畫幀率(FPS)計(jì)算:https://www.cnblogs.com/coco1s/p/8029582.html