Page Visibility API

深入理解 Page Visibility API:页面可见性检测、visibilitychange 事件、以及在后台标签页中优化应用行为。

Page Visibility API

一、Page Visibility API 概述

1.1 什么是页面可见性

Page Visibility API 允许开发者判断页面的当前可见性状态。当用户切换标签页、最小化窗口、或锁屏时,页面会进入"隐藏"状态。应用可以据此暂停或恢复某些行为。

// 检测页面可见性
if (document.hidden) {
  console.log('Page is hidden');
} else {
  console.log('Page is visible');
}

// 监听可见性变化
document.addEventListener('visibilitychange', () => {
  console.log('Visibility changed:', document.visibilityState);
});

1.2 可见性状态

状态 描述
visible 页面至少部分可见,是前台非最小化标签页
hidden 页面不可见:后台标签页、最小化窗口、或 OS 锁屏

二、核心 API

2.1 visibilityState 属性

console.log(document.visibilityState); // 'visible' | 'hidden'

2.2 hidden 属性

// 布尔值,更简洁的检查方式
if (document.hidden) {
  // 页面已隐藏
}

2.3 visibilitychange 事件

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    // 页面进入后台
    pauseAnimations();
    stopNetworkPolling();
  } else {
    // 页面恢复可见
    resumeAnimations();
    startNetworkPolling();
  }
});

三、实际应用

3.1 动画/视频暂停

// 标签页隐藏时暂停动画
class AnimationController {
  constructor() {
    this.isRunning = false;
    this.frameId = null;

    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        this.pause();
      } else {
        this.resume();
      }
    });
  }

  start() {
    this.isRunning = true;
    this.loop();
  }

  pause() {
    this.isRunning = false;
    if (this.frameId) {
      cancelAnimationFrame(this.frameId);
      this.frameId = null;
    }
  }

  resume() {
    if (!this.isRunning) {
      this.isRunning = true;
      this.loop();
    }
  }

  loop() {
    if (!this.isRunning) return;
    this.update();
    this.frameId = requestAnimationFrame(() => this.loop());
  }
}

3.2 网络轮询优化

class PollingService {
  constructor() {
    this.isPolling = false;
    this.intervalId = null;

    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        this.stop();
      } else {
        this.start();
      }
    });
  }

  start() {
    if (this.isPolling) return;
    this.isPolling = true;
    this.poll();

    this.intervalId = setInterval(() => {
      this.poll();
    }, 5000);
  }

  stop() {
    this.isPolling = false;
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }

  async poll() {
    // 获取最新数据
    const data = await fetch('/api/status').then(r => r.json());
    this.onUpdate(data);
  }

  onUpdate(data) {
    // 更新 UI
  }
}

3.3 游戏暂停

class Game {
  constructor() {
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        this.pause();
      } else {
        this.resume();
      }
    });
  }

  pause() {
    this.isPaused = true;
    this.lastTime = performance.now();
    // 保存游戏状态
    this.saveState();
  }

  resume() {
    if (!this.isPaused) return;
    this.isPaused = false;
    // 计算暂停时长
    const pauseDuration = performance.now() - this.lastTime;
    this.adjustTimers(pauseDuration);
  }
}

四、性能优化

4.1 后台标签页节流

浏览器自动对后台标签页进行节流,但应用可以进一步优化:

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    // 降低更新频率
    this.updateInterval = 1000; // 从 16ms 降到 1000ms
  } else {
    // 恢复正常频率
    this.updateInterval = 16;
  }
});

4.2 释放资源

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    // 释放大型资源
    this.largeData = null;
    this.imageCache.clear();

    // 暂停不必要的后台任务
    this.worker.terminate();
  } else {
    // 重新初始化
    this.initResources();
    this.initWorker();
  }
});

五、相关事件

5.1 与 beforeunload 的区别

事件 触发时机
visibilitychange 标签页切换、最小化、锁屏
beforeunload 页面即将关闭/刷新
pagehide 页面即将隐藏(可能被缓存)

5.2 与 requestAnimationFrame

// requestAnimationFrame 在后台自动暂停
// visibilitychange 提供更精细的控制

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    // visibilitychange 先于 rAF 暂停触发
    console.log('Will pause soon');
  }
});

六、面试高频问题

Q: visibilitychange 和 beforeunload 有什么区别?

回答要点:visibilitychange 在标签页切换、最小化、锁屏时触发,页面可能只是暂时不可见;beforeunload 在页面即将关闭或刷新时触发。visibilitychange 更适合需要区分"暂时隐藏"和"永久离开"的场景。

Q: 为什么后台标签页的 requestAnimationFrame 会暂停?

回答要点:这是浏览器的优化行为,避免后台标签页消耗资源。当标签页重新可见时,rAF 会恢复。这与 Page Visibility API 配合实现更智能的后台行为控制。

Q: Page Visibility API 的常见应用场景有哪些?

回答要点:视频/音频播放器在标签页隐藏时暂停;轮询服务在后台时停止以节省资源;游戏在标签页隐藏时暂停并保存状态;统计脚本避免在后台时发送无效曝光;动画在后台时暂停以节省 CPU。


参考资料

延展阅读