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。