跳至主要內容
Skip to content

頁面可見度 (Page Visibility)

你是否曾經遇過這種情況:打開了一堆分頁,其中一個網頁卻還在拼命播放影片、跑動畫,甚至不斷發送 API 請求,白白消耗你的筆電電力與網路流量?

這就是 Page Visibility API 登場的時刻。它讓我們能夠精準偵測網頁「是否被使用者看見」,進而做出智慧的效能優化決策。


一、 為什麼不用 focusblur

在 Page Visibility API 出現之前,開發者常使用 window.onfocuswindow.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支援

早期開發者需要處理 webkitmoz 前綴(如 document.webkitVisibilityState),但現在標準版 API 已經非常普及,直接使用 document.visibilityState 即可。


總結

概念說明
Visibility Statevisible (可見), hidden (完全不可見)
Visibility Change狀態改變時觸發的事件
效能優化隱藏時暫停高耗能任務(動畫、輪詢、影音)
使用者體驗避免在使用者看不到的時候發出聲音或消耗電力

TIP

最佳實踐:永遠考慮將耗能的操作與 visibilityState 綁定。這不僅是技術優化,更是對使用者裝置電量與數據流量的尊重。


進階挑戰

  1. 動態標題:當使用者離開分頁時,修改 document.title 來吸引他們回來(例如:「😭 不要走!」、「📩 你有新訊息」)。
  2. Web Socket 優化:在頁面隱藏過久後,考慮斷開 Socket 連線,回來時再重連。
  3. Analytics 修正:確保統計的使用者「停留時間」扣除掉頁面隱藏的時間,獲得更真實的數據。

延伸閱讀與資源