導讀: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已經引入了這些頁面的生命週期特性了。
概覽頁面生命週期和狀態
狀態包括:
- 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
狀態。 freeze
和resume
事件沒有被完全支持。- IE10- 不支持
visibilitychange
事件。 - 以前的瀏覽器,
visibilitychange
在pagehide
之後觸發,而chrome無視了document
在unload
的可見狀態,先觸發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
- 持久化動態的視圖狀態(如滾動高度)到
sessionStorage
或IndexedDB
當頁面從凍結態返回到
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
- 我的頁面要在
hidden
時仍然工作,怎麼阻止它被frozen
或者discarded
呢(比如音樂類APP)?
chrome只會在確保安全時凍結或丟棄它。在有以下資源使用時則不會:
- 播放音視頻
- 使用
WebRTC
- 更新表頭或favicon
- 彈
alert
- 發送
push notificatoins
- 什麼是
page navigation cache
(頁面導航緩存)?
這是一個通用名詞,用來描述瀏覽器對頁面導航的優化,讓前進後退按鈕更加快捷。webkit
把它叫做page cache
,火狐則成爲Back-Forwards Cache
。當導航離開時這些瀏覽器會凍結當前頁面以節約cpu和電量,因此在前進後退再進入這個頁面的時候,可以重新resume
。添加beforeunload
和unload
事件監聽器都會阻止瀏覽器所做的優化。
- 爲什麼沒有提到
laod
/DOMContentLoaded
事件呢?
頁面生命週期API要求狀態是離散而且獨立的。頁面可能以active
,passvie
,hidden
狀態載入(load),因此一個單獨的loading
狀態毫無意義。並且二者都不能指示着頁面生命週期的變化,所以與這些API無關。
- 如果我不能在凍結態和終止態去運行異步的api,那我怎麼把數據存到
IndexedDB
呢?
這確實是個問題。在frozen
和terminated
狀態,可凍結的任務會被暫停,所以異步的回調都不能保證可靠。
未來會在IDBTransaction
加入commit()
方法,保證開發者可以執行不需要回調的只寫型事務。也就是說,如果不需要讀,commit
方法可以在任務隊列被暫停前完成。
目前,開發者還有這兩種選擇:
- 使用
session storage
,這是同步的,頁面被丟棄也會持久化。 - 用
service worker
寫入IndexedDB
。可以在freeze
/pagehide
事件監聽器上通過postMessage()
給service worker
發送數據,讓後者來完成。但當存在內存壓力的時候,不建議使用後者。
測試你的app的frozen
和discarded
狀態
打開chrome://discards/來真正嘗試一下凍結和丟棄打開的標籤頁是怎麼回事兒吧~
同時還可以看看document.wasDiscarded
的值是否跟預期一致。
總結
爲了更合理地使用系統資源,開發者應善用頁面週期狀態。
另外,對瀏覽器而言,越多的開發者開始應用新的頁面週期API,凍結和丟棄頁面也會變得更安全可靠,從而節約內存,cpu,電量和網絡資源。
最後,如果不想記住和手寫這麼多API,可以嘗試pagelifecycle.js
這個庫。