前端记录页面停留时长

主要逻辑

1.进入页面开始计时 

2.如果浏览器tab切换,停止计时,重新切换回当前页面,继续累加计时 

3.如果浏览器缩小或被遮盖,停止计时,重新切换回当前页面,继续累加计时 

4.如果浏览器刷新,重新计时

5.如果浏览器关闭,停止计时

代码实现

基于以上逻辑,使用以下代码进行实现:

let startTime;
   let elapsedTime = 0;
   let timerInterval;
   let isTimerRunning = false;

   function startTimer() {
       if (!isTimerRunning) {
           startTime = new Date().getTime() / 1000 - elapsedTime;
           timerInterval = setInterval(updateTimer, 1000);
           isTimerRunning = true;
       }
   }

   function stopTimer() {
       if (isTimerRunning) {
           clearInterval(timerInterval);
           elapsedTime = new Date().getTime() / 1000 - startTime;
           isTimerRunning = false;
       }
   }

   function updateTimer() {
       const currentTime = new Date().getTime() / 1000;
       elapsedTime = currentTime - startTime;
       displayTime(elapsedTime);
   }

   function displayTime(time) {
       const timeString = `${Math.floor(time)}秒`;
       console.log('timeString', timeString);
   }

   // 监听页面可见性变化
   document.addEventListener('visibilitychange', () => {
       if (document.hidden) {
           stopTimer();
       } else {
           startTimer();
       }
   });

   // 页面加载完成后开始计时
   window.addEventListener('load', startTimer);
   // 监听页面关闭或刷新
   window.addEventListener('beforeunload', stopTimer);

经过测试,发现一个特殊情况:当缩小浏览器或者切换tab到其他页面(其他电脑应用)后,经过十几分钟(或者更长时间)后再重新回到当前计时页面,以上代码会出现一个时间偏差,例如原本离开的时间是6秒,再回来应该从第7秒开始累加,但回来后会从10秒或者20秒开始累加;

原因如下:

js执行的任务队列分别有微任务和宏任务两种,而setInterval 和setTimeout 属于宏任务,在浏览器页面的执行机制里,当页面失焦被置于后台时,会因为浏览器的性能机制导致被延迟执行,具体延迟执行时长是不确定的(取决于当前页面的代码逻辑和浏览器本身的调度):

img.png

img.png

根据以上的情况和原因说明,不使用定时器,改用requestAnimationFrame 方法来实现更精准的时长计算

为什么使用requestAnimationFrame 可以解决时长精准度的问题?

可以概括为:requestAnimationFrame与浏览器的渲染周期同步,这意味着它以与浏览器的渲染帧速率一致的一致速率(通常为每秒 60 次)调用。这种同步有助于减少时间测量的差异。且当浏览器被缩小,或切换tab,该事件会被暂停调用,重新激活页面时会再次以每秒60次的频率调用

官方说明:
img.png

以下是修改之后的代码:

let startTime = 0;
let pauseTime = 0;
let accumulatedTime = 0;
let timerId = null;
let isRunning = false;

function startTimer() {
    if (!isRunning) {
        startTime = performance.now();
        isRunning = true;
        requestAnimationFrame(updateTimer);
    }
}

function updateTimer(timestamp) {
    if (isRunning) {
        const currentTime = timestamp;
        const elapsed = Math.floor(currentTime - startTime + accumulatedTime);
        // 更新计时器的显示
        console.log(`Elapsed time: ${elapsed}ms:::`,elapsed/1000);
        timerId = requestAnimationFrame(updateTimer);
    }
}

function pauseTimer() {
    if (isRunning) {
        pauseTime = performance.now();
        accumulatedTime += pauseTime - startTime;
        isRunning = false;
        cancelAnimationFrame(timerId);
    }
}

// 监听浏览器的 visibilitychange 事件
document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
        pauseTimer();
    } else {
        startTimer();
    }
});
// 初始化计时器
startTimer();

在html里引用以上修改后的代码,未出现离开时间较长时间偏差的问题,特此记录。

requestAnimationFrame的浏览器兼容性(2024/7/3):
img.png

参考文档:

动画演示js的EventLoop:https://juejin.cn/post/6969028296893792286

requestAnimationFrame的使用场景举例:https://juejin.cn/post/7190728064458817591#heading-1