后台计时器罢工?我改用visibilitychange监听,代码从此‘永不停机’!

该问题的核心是浏览器对后台标签页的资源优化机制,现代浏览器(尤其是Chrome)为了节省电量、内存,会降低后台标签页的JavaScript执行频率,甚至冻结定时器。

一. 使用Visibility API 监听页面状态

1. 使用setInterval

  • 浏览器提供了visibilitychange事件和document.visibilityState属性来检测页面是否可见
  • 当页面切换到后台时,我们停止计时器并记录已运行时间
  • 当页面回到前台时,我们重新启动计时器,并调整开始时间以保持计时连续性
js 复制代码
// 定义计时器相关变量
let startTime          // 记录计时器开始时间(用于计算总运行时长)
let elapsedTime = 0    // 记录暂停时已经运行的时间(用于恢复时保持计时连续性)
let timerId              // 保存计时器ID(用于后续清除计时器)

// 启动计时器函数
function startTimer() {
  // 计算新的开始时间:当前时间减去已暂停的时间
  // 这样计时器恢复时看起来像是连续运行的,不会出现时间跳跃
  startTime = Date.now() - elapsedTime
  // 清除之前的计时器(如果有)
  if (timerId) clearInterval(timerId)
  // 启动新的计时器,每100毫秒执行一次updateTimer函数(你的具体逻辑函数)
  timerId = setInterval(updateTimer, 100)
}

// 停止计时器函数
function stopTimer() {
  // 清除当前运行的计时器
  clearInterval(timerId)
  // 记录暂停时已运行的时间:当前时间减去计时器开始时间
  // 这样下次重启时可以知道已经运行了多久,保持计时连续性
  elapsedTime = Date.now() - startTime
}

// 监听页面可见性变化事件
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') {
    // 页面从后台回到前台:重启计时器
    startTimer()
  } else {
    // 页面进入后台:停止计时器以节省资源
    stopTimer()
  }
})

2. 使用requestAnimationFrame

requestAnimationFrame是浏览器提供的专门用于优化动画效果的API,其核心原理是与屏幕刷新率同步执行回调函数,从而实现更流畅的动画效果‌。

工作原理

  • 以系统刷新频率(通常60Hz/16.7ms)为周期调用回调函数‌
  • 回调函数在浏览器重绘前执行,确保动画帧同步‌
  • 后台标签页中自动暂停执行以节省资源‌

对比setTimeout/setInterval

  • 定时器存在时间间隔不稳定问题(受JS单线程影响)‌
  • 无法保证与屏幕刷新同步,可能导致丢帧‌
  • 持续消耗资源(即使页面不可见)‌

使用方法

js 复制代码
function animate() {
  // 动画逻辑
  requestAnimationFrame(animate) // 递归调用
}
requestAnimationFrame(animate) // 启动动画

优势总结

  • requestAnimationFramesetInterval更高效,因为它与浏览器渲染周期同步
  • 浏览器会自动优化后台页面的requestAnimationFrame调用,进一步节省资源
  • 这种实现方式更精确,因为它是基于浏览器的重绘周期而非固定时间间隔
  • 同样保持了计时连续性,确保页面切换前后计时准确
js 复制代码
let startTime = Date.now()      // 记录计时器开始时间
let accumulatedTime = 0         // 累计运行时间(用于处理暂停)
let frameId = null              // 保存动画帧ID(用于取消请求)

// 计时器核心逻辑
function tick() {
  // 计算总运行时间:当前时间减去开始时间加上之前累计的时间
  const elapsed = Date.now() - startTime + accumulatedTime
  
  // 如果运行时间达到1秒(1000毫秒)
  if (elapsed >= 1000) {
    console.log('Timer tick')    // 执行定时任务
    accumulatedTime = elapsed - 1000 // 重置累计时间(减去已完成的一个周期)
    startTime = Date.now()       // 重置开始时间为当前时间
  }
  
  // 请求下一帧继续执行
  frameId = requestAnimationFrame(tick)
}

// 监听页面可见性变化
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') {
    // 页面变为可见
    if (!frameId) {
      // 如果计时器未运行,则重新启动
      startTime = Date.now() - accumulatedTime // 调整开始时间保持连续性
      frameId = requestAnimationFrame(tick)    // 启动计时器
    }
  } else {
    // 页面变为不可见
    accumulatedTime = Date.now() - startTime // 记录已运行时间
    cancelAnimationFrame(frameId)            // 取消动画帧请求
    frameId = null                           // 重置ID
  }
})

// 初始启动计时器
frameId = requestAnimationFrame(tick)

二. Web Workers 后台线程

  • Web Workers运行在独立线程中,不受主页面可见性状态影响
  • 计时逻辑在Worker线程中执行,即使页面在后台也能保持精确计时
  • Worker通过postMessage与主线程通信,主线程只在收到消息时才需要处理
  • 这种方式适合需要精确后台计时的场景,但会消耗更多资源
js 复制代码
// ========== 主线程代码 (main.js) ==========
// 创建一个Web Worker,指定worker脚本文件
// Web Worker是在独立线程中运行的JavaScript,不受主页面可见性影响
const worker = new Worker('timer-worker.js')

// 向worker发送消息,告诉它启动计时器,间隔1000毫秒
worker.postMessage({ 
  action: 'start',      // 操作类型:启动计时器
  interval: 1000        // 时间间隔(ms):1秒
});

// 监听worker发来的消息
worker.addEventListener('message', (e) => {
  if (e.data.tick) {
    // 收到tick消息时的处理逻辑
    console.log('Timer tick from worker')
    // 这里可以更新UI或执行其他需要主线程处理的任务
  }
})

// ========== Worker线程代码 (timer-worker.js) ==========
// 监听来自主线程的消息
self.addEventListener('message', (e) => {
  if (e.data.action === 'start') {
    // 在worker中启动计时器
    // 注意:Worker中的setInterval不受页面可见性影响
    setInterval(() => {
      // 每隔指定间隔向主线程发送tick消息
      self.postMessage({ tick: true })
    }, e.data.interval)
  }
})
相关推荐
Doris8931 小时前
【JS】Web APIs BOM与正则表达式详解
前端·javascript·正则表达式
晚霞的不甘1 小时前
实战进阶:构建高性能、高可用的 Flutter + OpenHarmony 车载 HMI 系统
开发语言·javascript·flutter
网络点点滴1 小时前
pinia简介
开发语言·javascript·vue.js
局i1 小时前
v-for 与 v-if 的羁绊:Vue 中列表渲染与条件判断的爱恨情仇
前端·javascript·vue.js
狮子座的男孩1 小时前
js函数高级:06、详解闭包(引入闭包、理解闭包、常见闭包、闭包作用、闭包生命周期、闭包应用、闭包缺点及解决方案)及相关面试题
前端·javascript·经验分享·闭包理解·常见闭包·闭包作用·闭包生命周期
风止何安啊2 小时前
从 “牵线木偶” 到 “独立个体”:JS 拷贝的爱恨情仇(浅拷贝 VS 深拷贝)
前端·javascript·面试
漫天黄叶远飞2 小时前
地址与地基:在 JavaScript 的堆栈迷宫里,重新理解“复制”的哲学
前端·javascript·面试
ohyeah2 小时前
深入理解 JavaScript 中的继承与 instanceof 原理
前端·javascript
crary,记忆2 小时前
React 之 useEffect
前端·javascript·学习·react.js