網頁生命週期 API

瀏覽器支援

  • Chrome:68。
  • Edge:79,
  • Firefox:不支援。
  • Safari:不支援。

現今的新式瀏覽器有時會在下列情況中將網頁停權或完全捨棄 系統資源有限日後,瀏覽器會 以降低耗電量和記憶體用量Page Lifecycle API 提供生命週期掛鉤,這樣網頁就能安全地處理這些瀏覽器 而不影響使用者體驗。來看看這個 API 查看是否應在應用程式中實作這些功能。

背景

應用程式生命週期是現代化作業系統管理的關鍵方式 再複習一下,機構節點 是所有 Google Cloud Platform 資源的根節點在 Android、iOS 和近期 Windows 版本上,可啟動應用程式 作業系統隨時停止。這讓這些平台能簡化及 並在最有利於使用者的位置重新分配資源

在網路上,向來沒有這樣的生命週期,應用程式可以保留 使應用程式失去生命有大量網頁正在執行時,關鍵系統 記憶體、CPU、電池和網路等資源可能會過度訂閱 導致使用者體驗不佳

網頁平台長期存在與生命週期狀態相關的事件 — 例如 loadunloadvisibilitychange — 這些事件僅供開發人員使用 來回應使用者啟動的生命週期狀態變更。適合透過網路工作 可以在低功耗裝置上穩定運作 (一般來說 所有平台) 瀏覽器需要主動收回和重新分配系統 再複習一下,機構節點 是所有 Google Cloud Platform 資源的根節點

事實上,瀏覽器目前已經會採取積極措施以節省資源 且許多瀏覽器 (尤其是 Chrome) 會 可以執行更多作業,減少整體資源佔用空間。

問題在於,開發人員無法因應這類技術 系統啟動的介入措施,甚至是知道他們正在演唱。也就是說 瀏覽器必須保守或有破壞網頁的風險。

Page Lifecycle API 可以透過以下方式解決這個問題:

  • 在網路上介紹及標準化生命週期狀態的概念。
  • 定義系統啟動的新狀態,讓瀏覽器 隱藏或閒置分頁可能會消耗的資源。
  • 建立新的 API 和事件,讓網頁開發人員能夠回應 在這些新系統啟動狀態間來回切換。

這項解決方案提供了網頁程式開發人員所需的可預測性 使應用程式不受系統介入影響,同時也能讓瀏覽器執行 積極改善系統資源,最終造福所有網路使用者。

本文的其餘部分將介紹新的「頁面生命週期」功能 探索它們與所有現有網路平台狀態的關係 和事件還會針對各類廣告活動提供建議和最佳做法 開發人員應 (且不該) 在各個狀態下執行的工作。

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

所有頁面生命週期狀態皆為獨立且互斥,亦即網頁 只能處於一個狀態。頁面生命週期狀態的大多數變更 通常可透過 DOM 事件觀察 (例外狀況請參閱開發人員建議針對每個狀態)。

也許是解釋頁面生命週期狀態最簡單的方式, 發出轉換信號的事件,如下圖所示:

以視覺化方式呈現本文件所說明的狀態和事件流程。
Page Lifecycle API 狀態和事件流程。

下表將詳細說明各種狀態。也會列出可能 狀態,而開發人員可以 來觀察變化

說明
進行中

如果網頁可供檢視且 輸入焦點

可能的舊狀態:
被動 (透過 focus 事件)
已凍結 (透過 resume 事件, pageshow 事件)

可能的後續狀態:
被動 (透過 blur 事件)

被動式

如果網頁可供檢視且確實存在,則表示網頁處於被動狀態 沒有輸入焦點

可能的舊狀態:
有效 (透過 blur 事件)
隱藏 (透過 visibilitychange 事件)
已凍結 (透過 resume 事件, pageshow 事件)

可能的後續狀態:
有效 (透過 focus 事件)
隱藏 (透過 visibilitychange 事件)

隱藏

如果網頁不顯示 (且尚未顯示) 處於隱藏狀態, 可能遭到凍結、捨棄或終止)。

可能的舊狀態:
被動 (透過 visibilitychange 事件)
已凍結 (透過 resume 事件, pageshow 事件)

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

凍結

在「凍結」狀態下,瀏覽器暫停執行 可凍結 項工作 工作佇列,直到頁面取消凍結為止。也就是說, JavaScript 計時器和擷取回呼不會執行。已在執行 完成工作 (最重要的是 freeze 回呼),但這些函式在執行時可能會受到限制 以及可放送的時間長度

瀏覽器凍結網頁以保留 CPU/電池/數據用量;他們 也有助於加快 往返導覽功能:避免顯示完整頁面 重新載入。

可能的舊狀態:
隱藏 (透過 freeze 事件)

可能的後續狀態:
有效 (透過 resume 事件, pageshow事件)
被動 (透過 resume 事件, pageshow 事件)
隱藏 (透過 resume 事件)
已捨棄 (未觸發任何事件)

已終止

網頁開始後,就會處於「已終止」狀態 瀏覽器從記憶體卸載並清除。否 新工作可以在這個狀態下展開,而進行中的工作也可能 或是終止系統

可能的舊狀態:
隱藏 (透過 pagehide 事件)

可能的後續狀態:

已捨棄

當網頁因 以節省資源不設定工作、事件回呼,或 任何類型的 JavaScript 都能在這個狀態中執行,因為通常會捨棄 發生於資源限制時,啟動新的程序時 不可能

「捨棄」狀態分頁本身 通常會向使用者顯示 (包括分頁標題與網站小圖示) 就算該網頁消失了

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

可能的後續狀態:

活動

瀏覽器會派出大量事件,但只有一小部分會發送 頁面生命週期狀態可能有所變更。下表列出所有事件 並列出可能轉換到哪些狀態。

名稱 詳細資料
focus

DOM 元素已接收焦點。

注意:focus 事件不會 便視為狀態發生變更只有在發生以下情況時,才會指出狀態變更: 而網頁先前沒有輸入焦點。

可能的舊狀態:
被動

目前可能的狀態:
有效

blur

DOM 元素已失去焦點。

注意:blur 事件不會 便視為狀態發生變更只有在發生以下情況時,才會指出狀態變更: 網頁不再有輸入焦點 (例如網頁並非只是切換 可聚焦於其他元素)。

可能的舊狀態:
有效

目前可能的狀態:
被動

visibilitychange

文件的 visibilityState的值已變更。這可以 會在使用者前往新分頁、切換分頁、關閉分頁時觸發 在行動作業系統上最小化或關閉瀏覽器,或切換應用程式 有些人會將 Cloud Storage 視為檔案系統 但實際上不是

可能的舊狀態:
被動
隱藏

目前可能的狀態:
被動
隱藏

freeze *

網頁剛才已凍結。不限 系統不會在頁面的工作佇列中啟動可釋放的工作。

可能的舊狀態:
隱藏

目前可能的狀態:
已凍結

resume *

瀏覽器已恢復凍結頁面。

可能的舊狀態:
已凍結

目前可能的狀態:
有效 (若後面接上 pageshow 事件)
被動 (若後面接上 pageshow 事件)
隱藏

pageshow

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

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

可能的舊狀態:
已凍結 (resume 事件也會觸發)

目前可能的狀態:
有效
被動
隱藏

pagehide

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

如果使用者正繼續前往其他網頁,而瀏覽器可以新增 目前頁面至往返 快取,以便稍後重複使用,事件的 persisted 屬性 為 true。如果設為 true,網頁會進入 凍結狀態,否則會進入 終止 狀態。

可能的舊狀態:
隱藏

目前可能的狀態:
已凍結 (event.persisted 為 true, freeze 事件後續追蹤)
已終止 (event.persisted 為 false, unload 後續追蹤事件)

beforeunload

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

重要事項:beforeunload 事件 只用來通知使用者尚未儲存變更。這些 系統就會儲存活動變更,然後移除活動。 在網頁上無條件加入,否則可能會導致成效 在某些情況下。請參閱舊版 API 一節

可能的舊狀態:
隱藏

目前可能的狀態:
已終止

unload

正在卸載頁面。

警告: 使用 unload 事件,因此不建議使用 且在某些情況下可能會對成效造成負面影響。詳情請參閱 舊版 API 專區 瞭解詳情。

可能的舊狀態:
隱藏

目前可能的狀態:
已終止

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

Chrome 68 新增的功能

上一張圖表顯示兩種狀態:系統啟動而非 使用者啟動:凍結已捨棄。 如前所述,瀏覽器現在偶爾會當機並捨棄 隱藏分頁 (可自行斟酌),但開發人員無法得知 推動相關工作

在 Chrome 68 版中,開發人員現在可以觀察隱藏分頁凍結的時間 監聽freezedocument 上的 resume 事件。

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

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

在 Chrome 68 版中,document 物件現在包含 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() 函式建構,可以觀察所有頁面 使用下列程式碼變更生命週期狀態。

// 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

active 狀態是對使用者而言最重要的時間,因此 網頁能吸引最多時間 能快速回應使用者輸入內容

凡是可能會封鎖主執行緒的非 UI 工作,都應將優先順序降低 到 閒置期間 卸載至網路工作站

Passive

在「被動」狀態中,使用者並未與網頁互動。 但仍可看見。也就是說,使用者介面更新和動畫應仍 提供流暢的體驗,但更新發生的時機並不重要。

如果網頁從「有效」變更為「被動」,表示 儲存未儲存應用程式狀態的最佳時機

Hidden

如果頁面從「被動」變更為「隱藏」,表示 否則使用者只有在重新載入網頁後,才會再次與應用程式互動。

轉換至「隱藏」通常也會是最後狀態變更 值得開發人員信賴的例子 (尤其是在 因為使用者可以關閉分頁或瀏覽器應用程式本身, beforeunloadpagehideunload 就不會觸發這類事件)。

這表示您應該將「隱藏」狀態視為 使用者的工作階段換句話說,請保留所有未儲存的應用程式狀態 傳送任何未送出的數據分析資料

您也應該停止更新 UI,因為使用者不會看見更新 ),您應該停止使用者不想執行的任何工作 在背景執行

Frozen

處於「凍結」狀態時 「凍結」工作 工作佇列會暫停,直到頁面凍結為止,這可能會 (例如網頁遭捨棄時)。

這表示頁面會從「隱藏」變更為「凍結」 請務必停止計時器或卸除任何連線 這可能會影響其他同一個來源開啟的分頁,也可能會影響 瀏覽器可以將網頁放在 往返快取

請特別注意以下事項:

您也應該保留所有動態檢視畫面狀態 (例如捲動位置) ) sessionStorage (或 IndexedDB (透過以下方式) commit()) 建立 系統加以捨棄並稍後重新載入

如果頁面從「凍結」切換回「隱藏」, 您可以重新開啟任何已關閉的連線,或重新發起輪詢 網頁在最初凍結時停止。

Terminated

一般而言,您不需要在頁面轉換時採取任何行動 設為 terminated 狀態

由於使用者動作會導致網頁無法載入 狀態從 hidden 狀態開始,再進入 terminated 狀態 隱藏狀態是工作階段結束邏輯 (例如 持續保留應用程式狀態和回報給數據分析) 執行任務

此外 (如同 「隱藏」狀態),開發人員必須瞭解 轉換至「終止」狀態並不可靠 多數情況下都能偵測出 (尤其是行動裝置) 所偵測的 呼叫終止事件 (例如 beforeunloadpagehideunload) 可能會遺失資料。

Discarded

開發人員無法觀察「捨棄」狀態 系統就會執行這些動作這是因為網頁通常 並在資源限制下捨棄頁面,解除凍結頁面 我們無法依照 大多數情況

因此,建議您為 即可將變更從「隱藏」變更為「凍結」,然後 對捨棄的網頁載入做出相應的調整 正在檢查「document.wasDiscarded」。

再次強調,因為生命週期事件的可靠性和排序並非如此 在所有瀏覽器中一致實作,讓您輕鬆遵循我們的建議 使用 PageLifecycle.js

應避免的舊版 Lifecycle API

請盡可能避免下列事件。

卸載事件

許多開發人員會將 unload 事件視為保證回呼,並將其做為 工作階段結束信號,可儲存狀態及傳送數據分析資料,但執行這項操作 主要是不可靠,特別是在行動裝置上!unload 事件並未 在許多一般卸載情況下觸發,包括從分頁關閉分頁 或從應用程式切換器關閉瀏覽器應用程式。

因此,建議各位善用 visibilitychange 事件,用來判斷工作階段 並將隱藏狀態視為隱藏狀態 儲存應用程式和使用者資料的最可靠時間

此外,只有已註冊的 unload 事件處理常式 (透過 onunloadaddEventListener()) 會 將網頁放在往返快取中,加快 返回和向前載入

建議在所有新版瀏覽器中一律使用 pagehide 事件,用於偵測可能的網頁卸載 (又稱 已終止) 狀態),而不是 unload 事件。如果發生以下情況: 需要支援 Internet Explorer 10 以下版本, 偵測 pagehide 事件,且只在瀏覽器不支援時使用 unload pagehide:

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.
    return (event.returnValue = true);
  }
});

反之,因為系統只會在以下內容新增 beforeunload 事件監聽器 ,如果不需要,則移除):

const beforeUnloadListener = (event) => {
  event.preventDefault();
  
  // Legacy support for older browsers.
  return (event.returnValue = true);
};

// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
  addEventListener('beforeunload', beforeUnloadListener);
});

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

常見問題

為什麼沒有顯示「載入中」該怎麼辦?

Page Lifecycle API 會將狀態定義為獨立且互斥的狀態。 由於網頁可處於有效、被動或隱藏狀態 由於容器可能會在完成載入前變更狀態,甚至終止 在這個範例中並不合理。

我的網頁在隱藏時有作用,我該如何讓頁面凍結或遭到捨棄?

有許多正當理由是網頁在執行時不應凍結 都能維持隱藏狀態最顯而易見的例子,就是播放音樂的應用程式。

在某些情況下,Chrome 可能會捨棄網頁, 例如,該表單包含已取消提交的使用者輸入內容 beforeunload 處理常式會在頁面卸載時發出警告。

目前 Chrome 在捨棄網頁和 但除非不確定並不會對使用者造成影響。例如, 觀察到在隱藏狀態中採取以下任一行動 ,除非在極大的資源限制下,否則不會遭到捨棄:

  • 正在播放音訊
  • 使用 WebRTC
  • 更新表格標題或網站小圖示
  • 顯示快訊
  • 傳送推播通知

瞭解目前用來判斷分頁是否可安全使用的清單功能 凍結或捨棄,請參閱:凍結和捨棄中

什麼是往返快取?

往返快取是用於描述 某些瀏覽器採用的瀏覽最佳化機制 快轉按鈕

使用者離開網頁時,這些瀏覽器會凍結 以便快速恢復運作,以備不時之需 按下「上一頁」或「下一頁」按鈕。請記住,新增 unload 事件處理常式可避免這項最佳化作業

無論意圖和用途為何,這項凍結功能的運作方式都相同 瀏覽器停止運作以節省 CPU 或電池用量;所以總共是 視為凍結生命週期狀態的一部分。

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

在凍結和已終止狀態中 可執行的工作 網頁的工作佇列中 暫停執行,這表示非同步和回呼型 API,例如 IndexedDB 並不能穩定地使用

日後,我們會IDBTransaction 物件中新增 commit() 方法, 可讓開發人員執行有效的純寫入交易 且不需回呼。也就是說,如果開發人員 資料傳送至 IndexedDB,且不執行包含讀取作業的複雜交易 commit() 方法會在工作佇列前完成 已暫停 (假設 IndexedDB 資料庫已開啟)。

不過,對於目前需要用的程式碼,開發人員有兩種做法:

  • 使用工作階段儲存空間: 工作階段儲存空間 即為同步性質,且會保留在不同頁面捨棄時保留。
  • 透過 Service Worker 使用 IndexedDB:服務工作處理程序可以將資料儲存在 頁面終止或捨棄後的 IndexedDB。在 freezepagehide 事件監聽器,您可以透過此事件將資料傳送至 Service Worker postMessage()、 而 Service Worker 可以處理儲存資料
,瞭解如何調查及移除這項存取權。

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

如要測試應用程式在凍結和捨棄狀態中的行為,請造訪 chrome://discards,即可實際凍結或捨棄 開啟分頁。

Chrome 捨棄使用者介面
Chrome 捨棄使用者介面

這可確保你的網頁正確處理 freezeresume。 事件以及 document.wasDiscarded 旗標,這在觸發特定動作後

摘要

開發人員希望尊重使用者裝置的系統資源 建構應用程式時,應考量頁面生命週期狀態。因此,請務必 網頁未耗用過多系統資源 不會預期使用者

越多開發人員開始導入新的 Page Lifecycle API,就越安全 會導致瀏覽器凍結並捨棄未使用的網頁。這個 因此瀏覽器會耗用較少的記憶體、CPU、電池和網路資源。 這對使用者來說是雙贏