原文连接javascript
最近作 web 性能采集分析,一直以为跟用户交互无关的采集都放在 onLoad
或 DOMContentLoaded
中很不合理。 一番搜索,发现 web 页面也是有生命周期的。一番研究,解决了如何避免干扰用户采集信息的困惑。 W3C 最新的规范 Page Lifecycle, 提供了一系列的生命周期钩子函数,方便开发者可以在不干扰用户交互的状况下监听处理一些操做。html
问题:如何利用生命周期优雅的处理上报分析数据,既能保证在某些场景下不漏报,又能尽量少的干扰用户?java
应用程序生命周期是现代操做系统管理资源的关键方法。在移动iOS、Android和最新的桌面系统中, apps 在任什么时候候都能被 OS 启动或关闭,生命周期使得这些系统 streamline
(流线型,使增产节约),从新分配资源更加合理高效,极大的优化了用户的体验。git
历史上,web 并无生命周期的概念,致使 web 应用能够一直存活占用系统资源。浏览器打开大量的 Tab 页, 关键系统资源如内存、CPU、电池和网络被过分占用而没法释放,致使系统卡顿。例如老版本的 Chrome 虽然性能 在当时的浏览器单页执行对比中一直是翘楚,但开多了页面,特别吃内存,得益于生命周期,能够合理的回收内存。github
而 web 平台长期以来都有与生命周期状态相关的事件,如 load
、unload
、visibilitychange
, 这些事件容许开发者监听生命周期状态的改变。对于移动设备特别是一些低端机型,浏览器须要一种主动回收内存和从新分配内存的方式。web
事实上,如今的浏览器已经采起了积极的措施来节省后台标签页的资源,许多浏览器但愿作更多的事情来减小它们的资源占用。ajax
问题是开发人员目前没有办法为这些类型的系统启动干预作好准备,甚至没法知道它们正在发生。这意味着浏览器须要保守,不然就有可能破坏网页。chrome
Page Lifecycle API
试图经过如下方式解决这些问题:api
该解决方案提供了web开发人员构建对系统干预具备弹性的应用程序所需的可预测性,并容许浏览器更积极地优化系统资源,最终使全部web用户受益。浏览器
本文的将介绍新的页面生命周期特性,并探讨它们与全部现有web平台状态和事件的关系。它还将为开发人员在每一个状态下应该(和不该该)作的工做类型提供建议和最佳实践。
全部页面生命周期状态都是离散和互斥的,这意味着一个页面一次只能处于一个状态。页面生命周期状态的大多数更改一般均可以经过DOM事件观察到(关于异常,请参见开发人员对每一个状态的建议)。
生命周期状态转变以及触发的事件
状态 | 描述 | 可能前一个的状态(触发事件) | 可能下一个状态(触发事件) |
---|---|---|---|
Active | 页面可见document.visibilityState === 'visible' 而且有 input focus |
1. passive (focus) | 1. passive (blur) |
Passive | 页面可见且没有input 处于 focus | 1. active (blur) 2. hidden (visibilitychange) |
1. active (focus) 2. hidden (visibilitychange) |
Hidden | 页面不可见document.visibilityState === 'hidden' 且不被冻结 |
1. passive (visibilitychange) | 1. passive (the visibilitychange) 2. frozen (freeze) 3. terminated (pagehide) |
Frozen | frozen 状态浏览器会挂起任务队列中可冻结任务的执行,这意味着例如 JS timer 或fetch 回调不会执行。正在执行的任务能被完成,可是可执行的操做和运行的时间会被限制。浏览器冻结是为了节约 CPU、内存、电量的消耗。同时使前进后退更加快速,避免从网络从新加载全量页面 |
1. hidden (freeze) | 1. active (resume -> pageshow) 2. passive (resume -> pageshow) 3. hidden (resume) |
Terminated | terminated 状态表示浏览器已卸载页面并回收了资源占用,不会有新的任务执行,已运行的长任务可能会被清除。 |
1. hidden (pagehide) | 无 |
Discarded | discarded 状态发生在系统资源受限,浏览器会主动卸载页面释放内存等资源用于新进/线程。该状态下任何任务、事件回调或任何类型的JS都没法执行。尽管页面不在了,但浏览器 Tab 页的标签名和 favicon用户仍可见 |
1. frozen (no events fired) | 无 |
下面描述了与生命周期相关的全部事件,并列出了它们可能转换的状态。
document.visibilityState
值变化。触发场景:persisted
为 true,反之为 false。persisted
为true。若是为true,则页面将进入 frozen
状态,不然将进入 terminated
状态。beforeunload
事件,仅用来提醒用户有未保存的数据改变,一旦数据保存完成,该监听事件回调应该移除。 不该该无条件地将它添加到页面中,由于这样作在某些状况下会损害性能。unload
事件,由于它不可靠,在某些状况下可能会影响性能。frozen 和 discarded 是系统行为而不是用户主动行为,现代浏览器在标签页不可见事,可能会主动冻结或废弃当前页。 开发人员并不能知道这二者的发生过程。
Chrome 68+ 中提供了freeze、resume 事件,当页面从 hidden 状态转变为冻结和非冻结状态,开发人员能够监听 document
得知。
document.addEventListener('freeze', (event) => {
// The page is now frozen.
});
document.addEventListener('resume', (event) => {
// The page has been unfrozen.
});
复制代码
而且提供了 document.wasDiscarded
属性来获取当前加载的页面,以前是否非可见时被废弃过。
if (document.wasDiscarded) {
// Page was previously discarded by the browser while in a hidden tab.
}
复制代码
获取 active
、passive
、 hidden
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};
复制代码
像 frozen
和 terminated
状态须要监听 freeze
、pagehide
事件获取。
// 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});
复制代码
上面代码作了三件事:
getState()
初始化状态logStateChange
函数接收下一个状态,如改变则 console捕获阶段
事件,一次调用 logStateChange
,传入状态改变。注意:上述 console 打印的顺序在不一样的浏览器中可能不一致。
{capture: true}
且都在 window
上监听事件target
pagehide
、pageshow
在 window
上触发visibilitychange
, freeze
, resume
在 document
上触发focus
、blur
在相应的 DOM 元素上触发window
没法实现因为生命周期API刚刚被引入,新的事件和DOM api并无在全部浏览器中实现。此外,全部浏览器实现并不一致。 例如:
blur
事件,意味着 active
状态不通过 passive
状态而直接变成了 hidden
page navigation cache
,Page Lifecycle API
把缓存的页面分类为冻结状态, 可是尚未实现freeze
,resume
等最新的 API,虽然非/冻结状态也能够经过 pageshow
,pagehide
事件监听到。pagehide
事件pagehide
、visibilitychange
触发顺序已改变。 当页面正在被卸载时,若是页面可见,会先触发 pagehide
在触发 visibilitychange
。 最新版本的 Chrome ,不管页面是否可见都会先触发 visibilitychange
在触发 pagehide
。pagehide
或 visibilitychange
。须要监听 beforeunload
来作兼容, beforeunload
须要在冒泡阶段结束才能知道状态是否变成 hidden
,所以容易被其余事件取消。推荐使用PageLifecycle.js,确保跨浏览器的一致性。
做为开发人员,理解页面生命周期状态并知道如何在代码中观察它们很重要,由于您应该(也不该该)执行的工做类型在很大程度上取决于您的页面处于什么状态。
例如,若是页面处于不可见状态,则向用户显示临时通知显然没有意义。虽然这个例子很明显,但还有一些不太明显的建议值得列举。
状态 | 建议 |
---|---|
Active | 该状态是对用户来讲最重要的阶段,此时最重要的就是响应用户输入。长时间阻塞主线程的非no-UI任务能够交给idle 时期或web worker 处理 |
Passive | 该状态下,用户没有与页面交互,可是他们仍然能够看到它。这意味着UI更新和动画应该仍然是平滑的,可是这些更新发生的时间不那么关键。当页面从 active 变为 passive 时,是存储未保存数据的好时机。 |
Hidden | 当 passive 转变为 hidden ,用户颇有可能再也不与页面交互直到从新加载。hidden 状态每每是开发人员能够信赖的最后状态,尤为在移动端,例如切换 APP 时beforeunload 、pagehide 和 unload 事件都不会触发。这意味着,对于开发人员应该把 hidden 状态当成是页面会话的最终状态。在此时应该持久化未保存的应用数据,采集上报分析数据。同时,你应该中止UI更新,由于用户已经看不到了。也该中止那些用户并不想在后台执行的任务,节省电量等资源。 |
Frozen | 在 frozen 状态,任务队列中可冻结的任务会被挂起,直到页面解冻(也许永远不会发生,例如页面被废弃discarded )。此时有必要中止全部的 timer 和关闭链接(IndexedDB、BroadcastChannel、WebRTC、Web Socket connections。释放Web Locks),不该该影响其余打开的同源页面或影响浏览器把页面存入缓存(page navigation cache)。你也应该持久化动态视图信息(例如无限滑动列表的滑动位置)到 sessionStorage或IndexedDB via commit(),以便discarded 和 reloaded以后重用。 当状态从新变回 hidden 时您能够从新打开任何关闭的链接,或从新启动最初冻结页面时中止的任何轮询。 |
Terminated | 当页面变成 terminated 状态,开发人员通常不须要作任何操做。由于用户主动卸载页面时总会在 terminated 以前经历 hidden 状态(页面刷新和跳转时不必定会触发 visibilitychange ,少部分浏览器实现了,大部分可能须要 pagehide 甚至beforeunload 或unload 来弥补这些场景),你应该在 hidden 状态执行页面会话的结束逻辑(持久化存储、上报分析数据)。开发人员必须认识到,在许多状况下(特别是在移动设备上),没法可靠地检测到终止状态,所以依赖终止事件(例如 beforeunload 、pagehide 和unload )可能会丢失数据。 |
Discarded | 开发人员没法观察到被废弃的状态。由于一般在系统资源受限下被废弃,在大多数状况下,仅仅为了容许脚本响应discard 事件而解冻页面是不可能的。所以,不必从hidden 更改成frozen 时作处理,能够在页面加载时检查 document.wasDiscarded ,来恢复以前被废弃的页面。 |
unload
事件当作页面结束的信号来保存状态或上报分析数据,但这样作很是不可靠,特别是在移动端。 unload
在许多典型的卸载状况下不会触发,例如经过移动设备的选项卡切换、关闭页面或系统切换器切换、关闭APP。visibilitychange
事件来肯定页面会话什么时候结束,并将 hidden
状态视为最后保存应用和用户数据的可靠时间。unload
会阻止浏览器把页面存入缓存(page navigation cache),影响浏览器前进后退的快速响应。pagehide
事件代替 onload
监测页面卸载(terminated)。onload
最多用来兼容IE10。const terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';
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.
}, {capture: true});
复制代码
// bad:无条件使用
addEventListener('beforeunload', (event) => {
// A function that returns `true` if the page has unsaved changes.
if (pageHasUnsavedChanges()) {
event.preventDefault();
return event.returnValue = 'Are you sure you want to exit?';
}
}, {capture: true});
复制代码
// good
const beforeUnloadListener = (event) => {
event.preventDefault();
return event.returnValue = 'Are you sure you want to exit?';
};
const unsavedChanges = [];
/** * @param {Symbol|Object} id A unique symbol or object identifying the *. pending state. This ID is required when removing the state later. */
function addUnsavedChanges(id) {
if(unsavedChanges.indexOf(id) > -1) return; // 重复退出
if (unsavedChanges.length === 0) { // 首次监听
addEventListener('beforeunload', onbeforeunload);
}
unsavedChanges.push(id);
}
/** * @param {Symbol|Object} id A unique symbol or object identifying the *. pending state. This ID is required when removing the state later. */
function removeUnsavedChanges(id) {
const idIndex = unsavedChanges.indexOf(id);
if (idIndex > -1) {
unsavedChanges.splice(idIndex, 1);
// If there's no more pending state, remove the event listener.
if (unsavedChanges.length === 0) {
removeEventListener('beforeunload', onbeforeunload);
}
}
}
复制代码
有不少合理的理由在页面不可见(hidden)状态不冻结(frozen)页面,例如APP正在播放音乐。
对于有些场景,浏览器放弃页面也存在风险,例如用户有未提交的输入或开发人员监听了beforeunload
事件以便提醒用户。
所以,浏览器策略会趋于保守,只有在明确不会影响用户的时候才会放弃页面。例如如下场景不会废弃页面(除非受到设备的资源限制)。
注意:对于更新标题或favicon以提醒用户未读通知的页面,建议使用 service worker
,这将容许Chrome冻结或放弃页面,但仍然显示对选项卡标题或favicon的更改。
页面导航缓存是一个通用术语,用于优化后退和前进按钮导航,利用缓存快速恢复先后页面。Webkit 称 Page Cache
,Firefox
称 Back-Forwards Cache
(bfcache)。
冻结是为了节省CPU/电池/内存,而缓存是为了重载时快速恢复,二者配合才能相得益彰。所以,该缓存被视为冻结生命周期状态的一部分。
注意:beforeunload
、unload
会阻止该项优化。
页面生命周期状态定义为离散和互斥的。因为页面能够在active
、passive
或 hidden
状态下加载,所以单独的加载状态没有意义, 而且因为 load
和 DOMContentLoaded
事件不表示生命周期状态更改,所以它们与生命周期无关。
在这两个状态,任务可能被挂起不执行,例如异步请求、基于回调的API等一样不会被执行。如下是一些建议
terminated
、discarded
状态时经过监听freeze
or pagehide
经过 postMessage()
用来保存数据。 (受限与设备资源,可能唤起service worker 会加剧设备负担)兼容性分析
load
、DOMContentLoaded
、beforeunload
、unload
中处理上报采集数据。visibilitychange
在各类切换APP、息屏时处理采集信息。pagehide
收集页面刷新导航跳转场景。beforeunload
兼容 Safari
关闭 Tab 和IE11如下版本的场景。function clear(fn) {
['visibilitychange', 'pagehide', 'beforeunload']
.forEach(event => window.removeEventListener(event, fn));
}
function collect() {
const data = { /* */ };
const str = JSON.stringify(data);
if('sendBeacon' in window.navigator) {
if( window.navigator.sendBeacon(url, str) ) {
clear(collect);
} else {
// 异步发请求失败
}
} else {
// todo 同步 ajax
clear(collect);
}
}
const isSafari = typeof safari === 'object' && safari.pushNotification;
const isIE10 = 'onpagehide' in window;
window.addEventListener(`visibilitychange`, collect, true);
!isIE10 && window.addEventListener(`pagehide`, collect, true);
if(isSafari || isIE10) {
window.addEventListener(`beforeunload`, collect, true);
}
复制代码
对于性能有极致追求的开发人员,开发时都应该考虑到页面的生命周期。在不须要的状况下不消耗设备资源对用户来讲是很是重要的。
此外越多的开发人员开始使用生命周期 APIs,浏览器处理冻结或废弃再也不使用的页面就越安全。 这意味着浏览器将会消耗更少的内存、CPU、电量、网络资源,这都将有利于用户。