網頁生命週期 API

Browser Support

  • Chrome: 68.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

如果系統資源有限,新式瀏覽器有時會暫停或完全捨棄網頁。未來瀏覽器會主動執行這項操作,以減少耗電量和記憶體用量。網頁生命週期 API 提供生命週期掛鉤,因此網頁可以安全地處理這些瀏覽器介入措施,不會影響使用者體驗。請參閱 API,瞭解是否應在應用程式中實作這些功能。

背景

應用程式生命週期是現代作業系統管理資源的主要方式。在 Android、iOS 和最新版 Windows 上,應用程式隨時可由 OS 啟動和停止。這些平台可藉此簡化資源並重新分配,以盡可能提升使用者體驗。

在網路上,過去沒有這類生命週期,應用程式可以無限期保持運作。如果網頁數量過多,記憶體、CPU、電池和網路等重要系統資源可能會過度使用,導致使用者體驗不佳。

雖然網頁平台長期以來都有與生命週期狀態相關的事件 (例如 loadunloadvisibilitychange),但這些事件只允許開發人員回應使用者發起的生命週期狀態變更。為了讓網頁在低功耗裝置上穩定運作 (並在所有平台上更有效率地運用資源),瀏覽器需要主動回收及重新分配系統資源。

事實上,瀏覽器目前已採取積極措施來節省資源,以供背景分頁使用。許多瀏覽器 (尤其是 Chrome) 希望能進一步減少整體資源用量。

問題在於開發人員無法為這類系統啟動的介入措施做好準備,甚至不知道這些措施正在進行。這表示瀏覽器必須保守行事,否則網頁可能會損壞。

網頁生命週期 API 嘗試透過下列方式解決這個問題:

  • 在網頁上導入並標準化生命週期狀態的概念。
  • 定義新的系統啟動狀態,讓瀏覽器限制隱藏或閒置分頁可耗用的資源。
  • 建立新的 API 和事件,讓網頁開發人員能夠回應這些新系統啟動狀態的轉換。

這項解決方案可提供網頁開發人員所需的預測功能,協助他們建構可抵禦系統干預的應用程式,並讓瀏覽器更積極地最佳化系統資源,最終造福所有網路使用者。

本文的其餘部分將介紹新的網頁生命週期功能,並探討這些功能與所有現有網頁平台狀態和事件的關係。此外,也會針對開發人員在各個階段應執行的工作類型提供建議和最佳做法。

網頁生命週期狀態和事件總覽

所有網頁生命週期狀態都是不連續且互斥,也就是說,網頁一次只能處於一種狀態。此外,網頁生命週期狀態的大部分變更通常可透過 DOM 事件觀察到 (例外狀況請參閱各狀態的開發人員建議)。

如要說明網頁生命週期狀態,以及標示狀態之間轉換的事件,最簡單的方法或許就是使用圖表:

以視覺化方式呈現本文所述的狀態和事件流程。
網頁生命週期 API 狀態和事件流程。

下表詳細說明每個狀態。此外,這份文件也列出可能的前後狀態,以及開發人員可用於觀察變更的事件。

說明
進行中

如果網頁可見且具有輸入焦點,即處於有效狀態。

可能的前一個狀態:
被動 (透過 focus 事件)
凍結 (透過 resume 事件,然後是 pageshow 事件)

可能的下一個狀態:
passive (透過 blur 事件)

被動

如果網頁可見,但沒有輸入焦點,則處於「被動」狀態。

可能的前一個狀態:
active (透過 blur 事件)
hidden (透過 visibilitychange 事件)
frozen (透過 resume 事件,然後是 pageshow 事件)

可能的下一個狀態:
active (透過 focus 事件)
hidden (透過 visibilitychange 事件)

隱藏

如果網頁不可見 (且未凍結、捨棄或終止),則處於隱藏狀態。

可能的前一個狀態:
被動 (透過 visibilitychange 事件)
凍結 (透過 resume 事件,然後是 pageshow 事件)

可能的下一個狀態:
被動 (透過 visibilitychange 事件)
凍結 (透過 freeze 事件)
已捨棄 (未觸發任何事件)
終止 (未觸發任何事件)

凍結

在「凍結」狀態下,瀏覽器會暫停執行頁面工作佇列中的可凍結工作,直到頁面解除凍結為止。也就是說,JavaScript 計時器和擷取回呼等項目不會執行。正在執行的工作可以完成 (最重要的是 freeze 回呼),但可執行的動作和執行時間可能會受到限制。

瀏覽器會凍結網頁,藉此節省 CPU/電池/資料用量;此外,瀏覽器也會凍結網頁,以便 加快返回/前進的瀏覽速度,避免重新載入整個網頁。

可能的前一個狀態:
hidden (透過 freeze 事件)

可能的下一個狀態:
active (透過 resume 事件,然後是 pageshow 事件)
passive (透過 resume 事件,然後是 pageshow 事件)
hidden (透過 resume 事件)
discarded (未觸發任何事件)

已終止

網頁開始卸載並由瀏覽器從記憶體中清除後,就會進入「終止」狀態。在此狀態下, 無法啟動新工作,如果正在執行的工作執行時間過長,可能會遭到終止。

可能的前一個狀態:
hidden (透過 pagehide 事件)

可能的後續狀態:

已捨棄

瀏覽器為了節省資源而卸載網頁時,網頁會處於「已捨棄」狀態。在此狀態下,任何工作、事件回呼或 JavaScript 都無法執行,因為捨棄通常是在資源受限的情況下發生,而這時無法啟動新程序。

在「已捨棄」狀態下,分頁本身 (包括分頁標題和網站圖示) 通常會向使用者顯示,即使網頁已消失也一樣。

可能的前一個狀態:
隱藏 (未觸發任何事件)
凍結 (未觸發任何事件)

可能的後續狀態:

事件

瀏覽器會傳送許多事件,但只有一小部分事件會發出信號,表示網頁生命週期狀態可能發生變化。下表列出所有與生命週期相關的事件,以及這些事件可能轉換的狀態。

名稱 詳細資料
focus

DOM 元素已取得焦點。

注意:focus 事件不一定會發出狀態變更信號。只有在網頁先前沒有輸入焦點時,才會發出狀態變更信號。

可能的前一個狀態:
passive

可能的目前狀態:
active

blur

DOM 元素已失去焦點。

注意:blur 事件不一定會發出狀態變更信號。只有在網頁不再具有輸入焦點時,才會發出狀態變更信號 (也就是說,網頁並非只是將焦點從一個元素切換到另一個元素)。

可能的前一個狀態:
active

可能的目前狀態:
passive

visibilitychange

文件的 visibilityState 值已變更。使用者前往新網頁、切換分頁、關閉分頁、縮小或關閉瀏覽器,或在行動作業系統上切換應用程式時,都可能發生這種情況。

可能的前一個狀態:
passive
hidden

可能的目前狀態:
passive
hidden

freeze *

頁面剛才已凍結。網頁工作佇列中任何可凍結的工作都不會啟動。

可能的前一個狀態:
hidden

可能的目前狀態:
frozen

resume *

瀏覽器已恢復凍結的頁面。

可能的前一個狀態:
frozen

目前可能的狀態:
active (如果後接 pageshow 事件)
passive (如果後接 pageshow 事件)
hidden

pageshow

系統正在瀏覽工作階段記錄項目。

這可能是全新載入的網頁,也可能是從往返快取中擷取的網頁。如果網頁是從往返快取中擷取,事件的 persisted 屬性為 true,否則為 false

可能的前一個狀態:
frozen (a resume event would have also fired)

可能的目前狀態:
active
passive
hidden

pagehide

正在從工作階段記錄項目遍歷。

如果使用者要前往其他網頁,且瀏覽器能夠將目前網頁新增至往返快取,以便日後重複使用,則事件的 persisted 屬性為 true。如果 true,頁面會進入「凍結」狀態,否則會進入「終止」狀態。

可能的前一個狀態:
hidden

目前可能狀態:
已凍結 (event.persisted 為 true, freeze 事件隨即發生)
已終止 (event.persisted 為 false, unload 事件隨即發生)

beforeunload

即將卸載視窗、文件及其資源。 此時文件仍會顯示,且活動仍可取消。

重要事項:beforeunload 事件只能用於提醒使用者有未儲存的變更。儲存這些變更後,活動應該就會移除。請勿無條件將其新增至網頁,否則在某些情況下可能會影響效能。詳情請參閱舊版 API 一節

可能的前一個狀態:
hidden

可能的目前狀態:
terminated

unload

正在卸載頁面。

警告: 我們一律不建議使用 unload 事件,因為這項事件不可靠,有時還會影響效能。詳情請參閱舊版 API 一節

可能的前一個狀態:
hidden

可能的目前狀態:
terminated

* 表示由 Page Lifecycle API 定義的新事件

Chrome 68 版新增的功能

上圖顯示兩種由系統而非使用者啟動的狀態:「凍結」和「捨棄」。如前所述,瀏覽器目前偶爾會凍結並捨棄隱藏的分頁 (由瀏覽器自行決定),但開發人員無法得知何時會發生這種情況。

在 Chrome 68 中,開發人員現在可以監聽 document 上的 freezeresume 事件,觀察隱藏分頁何時凍結和解除凍結。

document.addEventListener('freeze', (event) => {
  // The page is now frozen.
});

document.addEventListener('resume', (event) => {
  // The page has been unfrozen.
});

從 Chrome 68 開始,document 物件現在包含桌面版 Chrome 的 wasDiscarded 屬性 (Android 支援功能正在追蹤這個問題)。如要判斷網頁是否在隱藏分頁中遭到捨棄,您可以在網頁載入時檢查這個屬性的值 (注意:捨棄的網頁必須重新載入才能再次使用)。

if (document.wasDiscarded) {
  // Page was previously discarded by the browser while in a hidden tab.
}

如需在 freezeresume 事件中應執行的重要事項,以及如何處理和準備捨棄的網頁,請參閱各個狀態的開發人員建議

接下來的幾節將概略說明這些新功能如何融入現有的網路平台狀態和事件。

如何在程式碼中觀察網頁生命週期狀態

在「作用中」、「被動」和「隱藏」狀態中,您可以執行 JavaScript 程式碼,透過現有的網頁平台 API 判斷目前的網頁生命週期狀態。

const getState = () => {
  if (document.visibilityState === 'hidden') {
    return 'hidden';
  }
  if (document.hasFocus()) {
    return 'active';
  }
  return 'passive';
};

另一方面,凍結終止狀態只能在各自的事件監聽器 (freezepagehide) 中偵測到,因為狀態正在變更。

如何觀察狀態變化

以先前定義的 getState() 函式為基礎,您可以使用下列程式碼觀察所有 PageLifecycle 狀態變化。

// Stores the initial state using the `getState()` function (defined above).
let state = getState();

// Accepts a next state and, if there's been a state change, logs the
// change to the console. It also updates the `state` value defined above.
const logStateChange = (nextState) => {
  const prevState = state;
  if (nextState !== prevState) {
    console.log(`State change: ${prevState} >>> ${nextState}`);
    state = nextState;
  }
};

// Options used for all event listeners.
const opts = {capture: true};

// These lifecycle events can all use the same listener to observe state
// changes (they call the `getState()` function to determine the next state).
['pageshow', 'focus', 'blur', 'visibilitychange', 'resume'].forEach((type) => {
  window.addEventListener(type, () => logStateChange(getState()), opts);
});

// The next two listeners, on the other hand, can determine the next
// state from the event itself.
window.addEventListener('freeze', () => {
  // In the freeze event, the next state is always frozen.
  logStateChange('frozen');
}, opts);

window.addEventListener('pagehide', (event) => {
  // If the event's persisted property is `true` the page is about
  // to enter the back/forward cache, which is also in the frozen state.
  // If the event's persisted property is not `true` the page is
  // about to be unloaded.
  logStateChange(event.persisted ? 'frozen' : 'terminated');
}, opts);

這段程式碼會執行下列三項作業:

  • 使用 getState() 函式設定初始狀態。
  • 定義函式,接受下一個狀態,並在狀態變更時,將狀態變更記錄到控制台。
  • 為所有必要的生命週期事件新增擷取事件監聽器,進而呼叫 logStateChange(),並傳遞下一個狀態。

請注意,所有事件監聽器都會新增至 window,且都會傳遞 {capture: true}。原因如下:

  • 並非所有網頁生命週期事件都有相同的目標。pagehidepageshow 會在 window 上觸發;visibilitychangefreezeresume 會在 document 上觸發;focusblur 則會在各自的 DOM 元素上觸發。
  • 這些事件大多不會冒泡,因此無法將非擷取事件監聽器新增至常見的祖先元素,並觀察所有事件。
  • 擷取階段會在目標或泡泡階段之前執行,因此在該階段新增事件接聽程式,有助於確保這些程式會在其他程式碼取消事件之前執行。

各州開發人員建議

身為開發人員,瞭解網頁生命週期狀態知道如何在程式碼中觀察這些狀態,是相當重要的事,因為您應該 (和不應該) 執行的工作類型,很大程度上取決於網頁的狀態。

舉例來說,如果網頁處於隱藏狀態,向使用者顯示暫時性通知就顯然不合理。雖然這個例子相當明顯,但還有其他不那麼明顯的建議值得列舉。

開發人員建議
Active

活躍狀態是使用者最關鍵的時刻,因此也是網頁 回應使用者輸入內容最重要的時刻。

任何可能阻斷主執行緒的非 UI 工作,都應降低優先順序,改在 閒置期間執行,或 卸載至 Web Worker

Passive

被動狀態下,使用者不會與網頁互動,但仍可看到網頁。也就是說,UI 更新和動畫仍應流暢,但這些更新發生的時間點較不重要。

當網頁從「active」變更為「passive」時,就是保存未儲存應用程式狀態的好時機。

Hidden

當網頁從「被動」變更為「隱藏」時,使用者可能要等到網頁重新載入,才會再次與網頁互動。

轉換為「隱藏」狀態通常也是開發人員可穩定觀察到的最後一次狀態變更 (在行動裝置上尤其如此,因為使用者可以關閉分頁或瀏覽器應用程式本身,而系統不會在這些情況下觸發 beforeunloadpagehideunload 事件)。

也就是說,您應將 hidden 狀態視為使用者工作階段的可能結束狀態。換句話說,就是保留所有未儲存的應用程式狀態,並傳送所有未傳送的數據分析資料。

您也應停止更新 UI (因為使用者不會看到),並停止使用者不希望在背景執行的任何工作。

Frozen

處於「凍結」狀態時,

也就是說,當網頁從「隱藏」變更為「凍結」時,您必須停止所有計時器或終止所有連線,否則凍結可能會影響相同來源的其他開啟分頁,或影響瀏覽器將網頁放入「返回/快轉快取」的能力。

請特別注意以下事項:

您也應將所有動態檢視畫面狀態 (例如無限清單檢視畫面中的捲動位置) 持久儲存至 sessionStorage (或透過 commit() 儲存至 IndexedDB),以便在網頁遭到捨棄並於稍後重新載入時還原。

如果頁面從「凍結」狀態轉回「隱藏」狀態,您可以重新開啟所有已關閉的連線,或重新啟動頁面最初凍結時停止的輪詢。

Terminated

一般來說,網頁轉換為「已終止」狀態時,您不需要採取任何行動。

由於使用者動作導致卸載的網頁一律會先進入 hidden 狀態,再進入 terminated 狀態,因此應在 hidden 狀態執行工作階段結束邏輯 (例如保存應用程式狀態並向 Analytics 報告)。

此外 (如隱藏狀態的建議所述),開發人員務必瞭解,在許多情況下 (特別是行動裝置),系統無法可靠地偵測到轉換至「已終止」狀態,因此依賴終止事件 (例如 beforeunloadpagehideunload) 的開發人員可能會遺失資料。

Discarded

網頁遭到捨棄時,開發人員無法觀察到捨棄狀態。這是因為網頁通常會在資源受限時遭到捨棄,而為了允許指令碼因應捨棄事件執行而解除凍結網頁,在大多數情況下根本不可能。

因此,您應準備好因應從 hidden 變更為 frozen 而可能導致的捨棄作業,然後在網頁載入時檢查 document.wasDiscarded,對捨棄網頁的還原作業做出反應。

再次提醒,由於並非所有瀏覽器都一致實作生命週期事件的可靠性和排序,因此遵循表格中建議最簡單的方法就是使用 PageLifecycle.js

應避免使用的舊版生命週期 API

請盡可能避免下列事件。

卸載事件

許多開發人員會將 unload 事件視為保證的回呼,並當做工作階段結束信號,用來儲存狀態和傳送 Analytics 資料,但這麼做極不可靠,尤其是在行動裝置上!在許多典型的卸載情況下,unload 事件不會觸發,包括從行動裝置上的分頁切換器關閉分頁,或從應用程式切換器關閉瀏覽器應用程式。

因此,建議您一律依據 visibilitychange 事件判斷工作階段何時結束,並將隱藏狀態視為儲存應用程式和使用者資料的最後可靠時間

此外,只要註冊 unload 事件處理常式 (透過 onunloadaddEventListener()),瀏覽器就無法將網頁放入往返快取,導致往返載入速度變慢。

在所有新式瀏覽器中,建議一律使用 pagehide 事件偵測可能的網頁卸載 (又稱「終止」狀態),而非 unload 事件。如需支援 Internet Explorer 10 以下版本,您應偵測 pagehide 事件,且僅在瀏覽器不支援 pagehide 時使用 unload

const terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';

window.addEventListener(terminationEvent, (event) => {
  // Note: if the browser is able to cache the page, `event.persisted`
  // is `true`, and the state is frozen rather than terminated.
});

beforeunload 事件

beforeunload 事件與 unload 事件有類似的問題,因為從歷史來看,beforeunload 事件可能會導致網頁不符合往返快取資格。新式瀏覽器沒有這項限制。不過,部分瀏覽器會採取預防措施,嘗試將網頁放入返回/轉送快取時,不會觸發 beforeunload 事件,這表示該事件無法做為工作階段結束信號。此外,部分瀏覽器 (包括 Chrome) 必須先有使用者與網頁互動,才會允許觸發 beforeunload 事件,進一步影響可靠性。

beforeunloadunload 的其中一個差異是,beforeunload 有正當用途。舉例來說,您想警告使用者,如果繼續卸載頁面,未儲存的變更將會遺失。

由於使用 beforeunload 有正當理由,建議您在使用者有未儲存的變更時新增 beforeunload 監聽器,並在儲存變更後立即移除。

換句話說,請勿執行下列操作 (因為這會無條件新增 beforeunload 監聽器):

addEventListener('beforeunload', (event) => {
  // A function that returns `true` if the page has unsaved changes.
  if (pageHasUnsavedChanges()) {
    event.preventDefault();

    // Legacy support for older browsers.
    event.returnValue = true;
  }
});

請改為執行下列操作 (因為這只會在需要時新增 beforeunload 監聽器,並在不需要時移除):

const beforeUnloadListener = (event) => {
  event.preventDefault();

  // Legacy support for older browsers.
  event.returnValue = true;
};

// A function that adds a `beforeunload` listener if there are unsaved changes.
onPageHasUnsavedChanges(() => {
  addEventListener('beforeunload', beforeUnloadListener);
});

// A function that removes the `beforeunload` listener when the page's unsaved
// changes are resolved.
onAllChangesSaved(() => {
  removeEventListener('beforeunload', beforeUnloadListener);
});

常見問題

為什麼沒有「載入中」狀態?

Page Lifecycle API 定義的狀態是互斥的獨立狀態。 由於網頁可能以有效、被動或隱藏狀態載入,且可能在載入完成前變更狀態,甚至終止載入,因此在這個範例中,獨立的載入狀態沒有意義。

我的網頁在隱藏時會執行重要工作,如何避免網頁遭到凍結或捨棄?

網頁在隱藏狀態下執行時,不應凍結的原因有很多。最明顯的例子是播放音樂的應用程式。

如果網頁含有未提交的使用者輸入內容的表單,或是具有在網頁卸載時發出警告的 beforeunload 處理常式,Chrome 捨棄網頁可能會造成風險。

目前 Chrome 會謹慎捨棄網頁,只有在確定不會影響使用者時才會這麼做。舉例來說,如果網頁在隱藏狀態下執行下列任何動作,除非資源極度受限,否則不會遭到捨棄:

  • 播放音訊
  • 使用 WebRTC
  • 更新資料表標題或 Favicon
  • 顯示快訊
  • 傳送推播通知

如要查看目前用於判斷分頁是否可安全凍結或捨棄的清單功能,請參閱 Chrome 中的「凍結和捨棄的啟發式方法」。

什麼是往返快取?

往返快取是指某些瀏覽器實作的瀏覽最佳化功能,可加快使用返回和前進按鈕的速度。

當使用者離開網頁時,這些瀏覽器會凍結該網頁的版本,以便使用者使用返回或前進按鈕返回時,可以快速繼續瀏覽。請注意,加入unload事件處理常式會導致無法進行這項最佳化

就所有意圖和目的而言,這種凍結在功能上與瀏覽器為節省 CPU/電池而執行的凍結相同;因此,這項凍結視為凍結生命週期狀態的一部分。

如果無法在凍結或終止狀態下執行非同步 API,該如何將資料儲存至 IndexedDB?

在凍結和終止狀態下,網頁工作佇列中的可凍結工作會暫停,這表示無法可靠地使用非同步和回呼式 API。

雖然大多數 IndexedDB API 都是以回呼為基礎,但 IDBTransaction 介面上的 commit() 方法提供一種方式,可啟動有效交易的提交程序,而不必等待系統傳送未完成要求的事件。這樣一來,您就能在 freezevisibilitychange 事件監聽器中,以可靠的方式將資料儲存至 IndexedDB 資料庫,因為系統會立即執行提交作業,而不是在獨立工作中排隊。

在凍結和捨棄狀態下測試應用程式

如要測試應用程式在凍結和捨棄狀態下的行為,可以前往 chrome://discards 實際凍結或捨棄任何開啟的分頁。

Chrome Discards UI
Chrome Discards 使用者介面

這樣一來,您就能確保網頁在捨棄後重新載入時,正確處理 freezeresume 事件,以及 document.wasDiscarded 旗標。

摘要

開發人員如要尊重使用者裝置的系統資源,應建構應用程式時考量網頁生命週期狀態。網頁不得在使用者意想不到的情況下耗用過多系統資源,這點至關重要

開發人員開始導入新的網頁生命週期 API 後,瀏覽器就能更安全地凍結及捨棄未使用的網頁。這表示瀏覽器會耗用較少的記憶體、CPU、電池和網路資源,對使用者來說是一大優勢。