頁面可見度 (Page Visibility)
你是否曾經遇過這種情況:打開了一堆分頁,其中一個網頁卻還在拼命播放影片、跑動畫,甚至不斷發送 API 請求,白白消耗你的筆電電力與網路流量?
這就是 Page Visibility API 登場的時刻。它讓我們能夠精準偵測網頁「是否被使用者看見」,進而做出智慧的效能優化決策。
一、 為什麼不用 focus 與 blur?
在 Page Visibility API 出現之前,開發者常使用 window.onfocus 和 window.onblur 來判斷。但這種方式有明顯缺陷:
- 誤判:當使用者同時開啟兩個視窗並排(Split View)時,失去焦點的那個視窗其實「仍然可見」,但不應被視為隱藏。
- 不精準:當瀏覽器視窗被最小化,或切換到其他分頁時,
blur雖然會觸發,但語意上並不完全等於「不可見」。
Page Visibility API 解決了這個問題,它提供了一個明確的屬性 document.visibilityState,告訴你網頁現在是真的「可見 (visible)」還是「隱藏 (hidden)」。
二、 Page Visibility API 核心
2.1 核心屬性
typescript
// document.visibilityState 回傳值:
// 'visible' : 頁面內容至少部分可見
// 'hidden' : 頁面完全不可見(最小化、切換分頁、鎖定螢幕)
const state = document.visibilityState;2.2 監聽事件
typescript
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
console.log("歡迎回來!");
} else {
console.log("暫離中...");
}
});三、 設計 Composable 介面的
我們將封裝一個簡單好用的 usePageVisibility,提供響應式的狀態:
typescript
// types/usePageVisibility.ts
import type { Ref } from "vue";
export type VisibilityState = "visible" | "hidden" | "prerender";
export interface UsePageVisibilityReturn {
/** 原始可見度狀態 */
visibilityState: Ref<VisibilityState>;
/** 是否可見 (computed) */
isVisible: Ref<boolean>;
/** 是否隱藏 (computed) */
isHidden: Ref<boolean>;
}四、 實作 usePageVisibility Composable
實作非常直觀,我們只需要在掛載時監聽事件,卸載時移除即可。
typescript
// composables/usePageVisibility.ts
import { ref, computed, onMounted, onUnmounted } from "vue";
import type {
UsePageVisibilityReturn,
VisibilityState,
} from "@/types/usePageVisibility";
export function usePageVisibility(): UsePageVisibilityReturn {
const visibilityState = ref<VisibilityState>("visible");
function updateState() {
if (typeof document !== "undefined") {
visibilityState.value = document.visibilityState as VisibilityState;
}
}
onMounted(() => {
if (typeof document !== "undefined") {
updateState(); // 初始化狀態
document.addEventListener("visibilitychange", updateState);
}
});
onUnmounted(() => {
if (typeof document !== "undefined") {
document.removeEventListener("visibilitychange", updateState);
}
});
const isVisible = computed(() => visibilityState.value === "visible");
const isHidden = computed(() => visibilityState.value === "hidden");
return {
visibilityState,
isVisible,
isHidden,
};
}五、 實際應用範例
5.1 智慧暫停影片
當使用者切換分頁時,自動暫停影片播放;切回來時自動繼續。這在影音網站是非常貼心的功能。
vue
<script setup lang="ts">
import { ref, watch } from "vue";
import { usePageVisibility } from "@/composables/usePageVisibility";
const videoRef = ref<HTMLVideoElement | null>(null);
const { isHidden } = usePageVisibility();
watch(isHidden, (hidden) => {
if (!videoRef.value) return;
if (hidden) {
// 頁面隱藏:暫停播放
videoRef.value.pause();
console.log("背景執行:暫停影片");
} else {
// 頁面可見:繼續播放(可選,視需求而定)
// 注意:某些瀏覽器可能限制非靜音的自動播放
videoRef.value.play();
console.log("前景執行:繼續播放");
}
});
</script>
<template>
<video ref="videoRef" controls src="movie.mp4"></video>
</template>5.2 節省 API 輪詢 (Polling)
如果你的網頁會定期向伺服器請求資料(例如股票報價、即時聊天),在頁面不可見時應該停止或降低頻率,以節省伺服器資源與使用者流量。
typescript
// composables/useSmartPolling.ts
import { onMounted, onUnmounted } from "vue";
import { usePageVisibility } from "./usePageVisibility";
export function useSmartPolling(callback: () => void, interval = 3000) {
const { isVisible } = usePageVisibility();
let timer: any = null;
function start() {
if (timer) return;
callback(); // 立即執行一次
timer = setInterval(() => {
// 只有在可見時才執行回呼
if (isVisible.value) {
callback();
}
}, interval);
}
function stop() {
if (timer) clearInterval(timer);
timer = null;
}
onMounted(start);
onUnmounted(stop);
}使用方式:
vue
<script setup lang="ts">
import { useSmartPolling } from "@/composables/useSmartPolling";
useSmartPolling(() => {
console.log("Fetching new data...");
// fetch('/api/data')
}, 5000);
</script>六、 瀏覽器支援度
Page Visibility API 的支援度極佳,幾乎所有現代瀏覽器都支援(包括 IE10+)。
| 瀏覽器 | 支援狀況 |
|---|---|
| Chrome | 支援 |
| Firefox | 支援 |
| Safari | 支援 |
| Edge | 支援 |
| iOS Safari | 支援 |
早期開發者需要處理 webkit 或 moz 前綴(如 document.webkitVisibilityState),但現在標準版 API 已經非常普及,直接使用 document.visibilityState 即可。
總結
| 概念 | 說明 |
|---|---|
| Visibility State | visible (可見), hidden (完全不可見) |
| Visibility Change | 狀態改變時觸發的事件 |
| 效能優化 | 隱藏時暫停高耗能任務(動畫、輪詢、影音) |
| 使用者體驗 | 避免在使用者看不到的時候發出聲音或消耗電力 |
TIP
最佳實踐:永遠考慮將耗能的操作與 visibilityState 綁定。這不僅是技術優化,更是對使用者裝置電量與數據流量的尊重。
進階挑戰
- 動態標題:當使用者離開分頁時,修改
document.title來吸引他們回來(例如:「😭 不要走!」、「📩 你有新訊息」)。 - Web Socket 優化:在頁面隱藏過久後,考慮斷開 Socket 連線,回來時再重連。
- Analytics 修正:確保統計的使用者「停留時間」扣除掉頁面隱藏的時間,獲得更真實的數據。