頁面的生命週期API及給開發者的建議------來自谷歌開發者博客

導讀:web平臺很早就有了生命週期的概念,如load, unload, visibilitychange,但這些時間只能讓開發者響應用戶發起的生命週期變化。爲了更合理地使用系統資源,開發者應善用頁面週期狀態。另外,對瀏覽器而言,越多的開發者開始應用新的頁面週期API,凍結和丟棄頁面也會變得更安全可靠,從而節約內存,cpu,電量和網絡資源。

背景

在安卓,ios和較新的的windows平臺上,操作系統對app有啓動和運用的權限,這些平臺會合理地爲應用分配資源。但是由於歷史原因,web上的app可以永遠保持活躍狀態。因此,如果有大量的頁面在同時運行,關鍵的系統資源如內存,cpu,電池,和網絡資源會被過度索取,造成很差的用戶體驗。
web平臺很早就有了生命週期的概念,如load, unload, visibilitychange,但這些時間只能讓開發者響應用戶發起的生命週期變化。對那些性能很差的設備,瀏覽器需要提前知道這件事,以便更合理地回收和重新分配系統資源。實際上,現代瀏覽器已經在這麼做了。但還有更多可以優化的,它們也還想做更多。問題在於,開發者並不清楚這些機制,所以瀏覽器還是得采取保守做法,或者冒着頁面崩潰的危險。

頁面生命週期API嘗試通過以下方式去解決這個問題:

  • 引入並標準化生命週期狀態的概念
  • 定義新的,系統啓動的生命週期狀態(new system-initialted states),允許瀏覽器限制非激活狀態或者被隱藏的頁面佔用資源
  • 建立新的api和事件,讓web開發者可以響應生命週期狀態的改變。

chrome 68已經引入了這些頁面的生命週期特性了。

概覽頁面生命週期和狀態

Page Lifecycle states

狀態包括:

  • active
  • passive
  • hidden
  • frozen 瀏覽器停止執行可凍結的事件,比如js計時器和fetch的回調,都不會再進行了。這是一種節約資源的手段。
  • terminated 頁面一旦開始unload,並從內存中被瀏覽器清掉,就是被terminated(終結)了。
  • discarded

事件包括(斜體爲新出的api):

  • focus
  • blur
  • visibilitychange
  • freeze 任務不會再執行
  • resume 瀏覽器重新啓動了一個凍結的頁面
  • pageshow
  • pagehide
  • beforeunload 僅僅用於提醒用戶別忘了保存,不可濫用!
  • unload 永遠不要使用這個事件!

frozen和discarded都是系統發起的狀態,而不是用戶發起的。如前所述,當今的瀏覽器可能會偶爾凍結或者丟棄了隱藏的tab,但開發者對此一無所知。所以在chrome68中,新引入了document上的freeze, resume這兩個事件,以讓開發者監聽。

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

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

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

檢測生命週期

在active, passive, hidden狀態下,可以通過js代碼來判斷當前的生命週期狀態。

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

frozen和terminated只能觀測他們相對的freeze/pagehide事件

asdfasd

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

// 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()), {capture: true});
});

// 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');
}, {capture: true});

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

這段代碼已經很清晰了,注意這裏是在捕獲階段監聽的。爲什麼要這麼做呢?

  • 沒有共同的觸發對象。這些事件中, pagehide/pageshow 在window上觸發,visibilitychange,freeze, resume在document上觸發,focus和blur在對應的dom元素上觸發
  • 大部分事件都不會冒泡。
  • 捕獲階段在target/冒泡階段之前,所以在這裏加入監聽保證了他們會在其他可能取消這一事件的代碼前執行。

跨瀏覽器差異

瀏覽器對上述API的實現還存在差異,例如:

  • 一些瀏覽器在切換標籤頁的時候不會觸發blur事件。這意味着一個頁面可能直接由active狀態變爲了hidden狀態。而跳過了passive狀態。
  • freezeresume事件沒有被完全支持。
  • IE10- 不支持visibilitychange事件。
  • 以前的瀏覽器,visibilitychangepagehide之後觸發,而chrome無視了documentunload的可見狀態,先觸發visibilitychange事件,再觸發pagehide事件。

這一切都可以通過一個js庫來解決:PageLifecycle.js

開發者應該在什麼state做什麼事

  • active: 響應用戶輸入行爲的最重要時機。任何會阻礙主線程的非UI行爲應該放到這之後來做。

  • passive: 在passive狀態用戶沒有跟頁面交互,但頁面仍然可見。這意味着UI的更新和動畫仍然應該流暢進行,但更新的時機就沒那麼重要了。頁面從active變到passive也是去保存應用狀態的最佳時機

  • hidden: 這可能是開發者能可靠地檢測到的最後一次狀態改變了,因爲用戶可能直接關閉了瀏覽器或應用。諸如beforeunload, pagehide, unload事件,在這種情況下都不會被觸發了。因此應該把hidden state當做用戶session的結束點。換句話說,持久化那些未被保存的應用狀態,併發送數據調查數據。停止UI更新和任何用戶不希望在後臺運行的任務。

  • frozen: 可以被凍結的任務都會被暫停,直到頁面解凍(可能永遠都不會解凍了,嚶嚶嚶)。應該阻止任何的計時器,切斷可能會影響其他開啓的同源Tab的連接。具體來說,需要:

    • 關閉所有開啓的IndexedDB的連接
    • 關閉所有開啓的BroadcasrChannel的連接
    • 關閉所有激活態的webRTC連接
    • 關閉所有的web Socket連接
    • 釋放所有可能拿着的Web Locks
    • 持久化動態的視圖狀態(如滾動高度)到sessionStorageIndexedDB

    當頁面從凍結態返回到hidden狀態時,重連上述連接。

  • terminated: 不做任何事,不做任何事,不做任何事。beforeunload, pagehide, unload都不能被可靠地監聽到。

  • discarded: 對開發者不可見。可以在一個被丟棄的頁面重新加載的時候檢測document.wasDiscarded

避免使用廢棄的生命週期API

  • unload: 宜用visibilitychange事件取代來判斷何時session終止,用hidden狀態作爲最後保存應用和用戶數據的可靠之機。
  • beforeunload: 和unload事件有同樣的問題,會組織瀏覽器在page navigation cache中緩存頁面。僅當提示用戶還有未保存的變化時調用,並且在保存後立即移除

正確操作:

const beforeUnloadListener = (event) => {
  event.preventDefault();
  return event.returnValue = 'Are you sure you want to exit?';
};

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

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

pagelifecycle.js庫已經提供了addUnsavedChanges()removeUnsavedChanges()方法

FAQs

  1. 我的頁面要在hidden時仍然工作,怎麼阻止它被frozen或者discarded呢(比如音樂類APP)?

chrome只會在確保安全時凍結或丟棄它。在有以下資源使用時則不會:

  • 播放音視頻
  • 使用WebRTC
  • 更新表頭或favicon
  • alert
  • 發送push notificatoins
  1. 什麼是page navigation cache(頁面導航緩存)?

這是一個通用名詞,用來描述瀏覽器對頁面導航的優化,讓前進後退按鈕更加快捷。webkit把它叫做page cache,火狐則成爲Back-Forwards Cache。當導航離開時這些瀏覽器會凍結當前頁面以節約cpu和電量,因此在前進後退再進入這個頁面的時候,可以重新resume。添加beforeunloadunload事件監聽器都會阻止瀏覽器所做的優化。

  1. 爲什麼沒有提到laod/DOMContentLoaded事件呢?

頁面生命週期API要求狀態是離散而且獨立的。頁面可能以activepassviehidden狀態載入(load),因此一個單獨的loading狀態毫無意義。並且二者都不能指示着頁面生命週期的變化,所以與這些API無關。

  1. 如果我不能在凍結態和終止態去運行異步的api,那我怎麼把數據存到IndexedDB呢?

這確實是個問題。在frozenterminated狀態,可凍結的任務會被暫停,所以異步的回調都不能保證可靠。
未來會在IDBTransaction加入commit()方法,保證開發者可以執行不需要回調的只寫型事務。也就是說,如果不需要讀,commit方法可以在任務隊列被暫停前完成。
目前,開發者還有這兩種選擇:

  • 使用session storage,這是同步的,頁面被丟棄也會持久化。
  • service worker寫入IndexedDB。可以在freeze/pagehide事件監聽器上通過postMessage()service worker發送數據,讓後者來完成。但當存在內存壓力的時候,不建議使用後者。

測試你的app的frozendiscarded狀態

打開chrome://discards/來真正嘗試一下凍結和丟棄打開的標籤頁是怎麼回事兒吧~

同時還可以看看document.wasDiscarded的值是否跟預期一致。

總結

爲了更合理地使用系統資源,開發者應善用頁面週期狀態。

另外,對瀏覽器而言,越多的開發者開始應用新的頁面週期API,凍結和丟棄頁面也會變得更安全可靠,從而節約內存,cpu,電量和網絡資源。

最後,如果不想記住和手寫這麼多API,可以嘗試pagelifecycle.js這個庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章