正文
今天的现代浏览器有时在系统资源受限的情境下会暂停页面或完全放弃执行它。将来,浏览器会主动执行此操作,因此它们会消耗更少的电量和内存。在Chrome 68中提供的
Page Lifecycle API
提供了生命周期钩子,因此网页可以安全地处理这些浏览器干预,而不会影响用户体验。具体请查看API了解你的应用程序是否需要实现这些特性。
背景
应用程序的生命周期是现代操作系统管理资源的关键。在Android, iOS, 和最近的Windows版本中,操作系统可以随时开始或结束应用程序。这使得这些平台可以简化和重新分配最有利于用户的资源。
在网络上, 有史以来从来没有过这样的生命周期, 所以应用程序可以一直保持运行态。运行大量网页后,内存,CPU,电池和网络等关键系统资源可能会超负荷运行,从而导致最终用户体验不佳。
虽然web平台早就有与生命周期状态相关的事件 — 如
load
,
unload
, and
visibilitychange
—这些事件只允许开发者响应用户造成的生命周期状态更改。为了使Web能够在低功耗设备上可靠地工作(并且通常在所有平台上具有更高的资源意识),浏览器需要一种主动回收和重新分配系统资源的方法。
事实上,现在的浏览器已经对在后台标签的页面
采取了积极措施来节约资源
, 而且很多的浏览器(特别是Chrome)会做更多这方面的工作 - 以减少它们的整体资源占用。
问题是开发者目前无法为系统执行的这类干预操作做好准备,甚至都不知道系统正在干预。 这意味着浏览器需要保护或冒险破坏网页。
Page Lifecycle API
通过以下措施来解决这个问题:
这种解决方法提供了可预测性,网页开发人员需要创建一个能灵活应对系统干预的应用程序,而且这种解决方法让浏览器可以更加积极地优化系统资源,最终令所有web用户受益。
本篇文章剩余部分会介绍Chrome 68中新的页面生命周期特性,并且会探索这些特性与所有已存在的网页平台的状态和事件的关联。文章也为开发者应该(或不应该)在每个状态进行的工作类型提供建议和最佳实践。
总览页面生命周期状态和事件
所有的页面生命周期状态都是相互独立存在的, 也就是说一个页面在同一个时间点只能存在一个状态。而且通常大多数页面生命周期的状态改变都可通过DOM事件监听 ( 也有例外,查看
开发者对每个状态的建议
).
也许图表是最能直观解释页面生命周期状态的,它同样也能很好的标识事件间的转换:
状态
下表详列了每个状态的细节信息。还列出了可能发生的前后状态,还包括开发者可以用来监听变化的事件。
当页面可见或有input框聚焦时,该页面处于
active
状态
可能之前的状态为:
passive
(通过
focus
事件转换而来)
可能下个状态为:
passive
(通过
blur
事件转换而来)
| |
Passive
|
当页面可见但没有聚焦的input框时,该页面处于
passive
状态
之前可能的状态是:
active
(通过触发
blur
事件而来)
hidden
(通过
visibilitychange
触发事件而来)
接下来可能变成的状态是:
active
(通过触发
focus
事件而来)
hidden
(通过触发
visibilitychange
事件而来)
| |
Hidden
|
如果页面不可见且尚未被冻结,则处于
hidden
状态。
之前可能的状态是:
passive
(通过事件
visibilitychange
而来)
接下来可能变成的状态:
passive
(通过触发
visibilitychange
事件而来)
frozen
(通过触发
freeze
事件而来)
terminated
(通过触发
pagehide
事件而来)
| |
Frozen
|
当处于
frozen
状态时,浏览器暂停执行页面里
任务队列
中
可冻结的
任务
直到页面不是冻结状态. 也就是说像JavaScript定时器和fetch回调不会运行。已经运行的任务可以结束(特别是
freeze
回调), 但它们可能会受限于它们做的是什么或能运行多久。
浏览器冻结页面是为了保护CPU/电池/数据使用,同样它也能使
后退/前进导航
更快— 因为避免了重新加载整个页。
可能之前的状态为:
hidden
(通过触发
freeze
事件而来)
_
之后可能变成的状态:
active
(通过先触发
resume
事件, 再触发
pageshow
事件而来)
passive
(通过先触发
resume
事件,再触发
pageshow
事件而来)
hidden
(通过触发
resume
事件而来)
_ | |
Terminated
|
一旦浏览器卸载页面或在内存中清除页面时,页面就变为
terminated
状态. 在这种状态下不会运行
新任务
, 并且正在进行的任务如果运行了太久也会被杀掉。
之前可能的状态是:
hidden
(通过触发
pagehide
事件)
之后可能的状态是:
NONE
| |
Discarded
|
当页面被浏览器卸载为保护资源时,页面处于
discarded
状态.在这种状态下不会运行任何任务、事件回调甚至是JavaScript。因为在资源受限的情况下通常要放弃某些操作, 所以不可能开启一个新进程。
处在
discarded
状态的tab页 (
包括tab页窗口的标题和图标
) 即使是页面消失也总是对用户可见
之前可能的状态是:
frozen
(不触发事件)
之后可能的状态是:
NONE
|
事件
浏览器会发送许多事件,但是只有小部分事件表明页面周期状态可能发生变化。下表概述了与生命周期相关的所有事件,并列出了它们可能转换前后的状态。
有DOM元素已经聚焦。
备注:
focus事件并不一定表示状态改变了。如果页面之前没有input聚焦,它仅表示状态更改。
之前可能的状态是:
passive
当前可能的状态:
active
| |
blur
|
有DOM元素失去焦点。
备注:
blur事件并不一定表示状态改变了。如果页面不再有input框聚焦(例如, 页面没有从一个聚焦的元素切换到另一个元素), 它仅表示状态更改。
之前可能的状态是:
active
当前可能的状态:
passive
| |
visibilitychange
|
文档上,
visibilityState
值已经更新了。文档的visibilityState值已更改。 当用户导航到新页面,切换选项卡,关闭选项卡,最小化或关闭浏览器或在移动端切换应用程序时,可能会使visibilityState的值改变。
之前可能的状态是:
passive
hidden
当前可能的状态:
passive
hidden
| |
freeze
*
|
页面刚被冻结. 页面任务队列不会执行任何
冻结的
任务。
之前可能的状态:
hidden
当前可能的状态:
frozen
| |
resume
*
|
The browser has resumed a
frozen
page. 浏览器已经恢复了
冻结的
页面
之前可能的状态:
frozen
当前可能的状态:
active
(如果是紧随
pageshow
事件发生)
passive
(如果是紧随
pageshow
事件发生)
hidden
| |
pageshow
|
会话历史记录新增一条记录。
这可能是个全新的页面,也可能是
导航缓存中的页面
. 如果页面是页面导航缓存中,事件的持久属性为true,否则为false。
可能之前的状态:
frozen
(
resume
事件也会被触发)
当前的状态:
active
passive
hidden
| |
pagehide
|
会话历史记录新增一条记录。
如果用户在浏览另一个页面, 而且浏览器可能会添加当前的页面到
页面导航缓存
,以便之后调用, 事件属性会持续为true,此时页面将进入到
frozen
状态, 否则会进入到结束状态。
可能之前的状态是:
hidden
当前可能的状态是:
frozen
(event.persisted为true,
freeze
事件紧随)
terminated
(event.persisted为false,
unload
事件紧随)
| |
beforeunload
|
window、document以及其资源即将被卸载。但在此时,document仍然是可见的,事件仍可以被取消。
警告:
beforeunload事件只能用于警告用户有未保存的改变。一旦保存后,事件应该会被清除。这样做不可能对页面没有影响,因为在某些场景下会牺牲性能。在
旧版API
看详细内容.
之前可能的状态是:
hidden
当前可能的状态是:
terminated
| |
unload
|
此时页面正在卸载
警告:
建议千万不要使用unload事件,因为它不稳定而且在某些场合下可能伤害性能。在
旧版API
看详细内容.
之前可能的状态是:
hidden
当前可能的状态:
terminated
|
*
以下展示页面生命周期API定义的新事件
在Chrome 68添加的新属性
上面的部分展示了两种状态,它是系统初始化状态而非用户初始化状态:
frozen
和
discarded
.
正如以上提到的,现在的浏览器偶尔会冻结并丢弃隐藏的标签(他们自己决定), 但是开发者无法得知。
在Chrome 68, 开发者现在可以通过监听document的
freeze
和
resume
事件来观察一个隐藏的tab标签什么时候冻结和解除冻结的.
document.addEventListener('freeze', (event) => {
// The page is now frozen.
});
document.addEventListener('resume', (event) => {
// The page has been unfrozen.
});
在chrome 68的文档对象中现在也包含了
wasDiscarded
属性. 它用于决定在隐藏的标签中何时被抛弃。你可以在页面加载时检查这个值(备注:被抛弃后的页面要重新用必须重新加载).
if (document.wasDiscarded) {
// Page was previously discarded by the browser while in a hidden tab.
}
若想了解关于在freeze和resume事件发生时该做哪些重要操作的建议,或想知道页面即将被抛弃时如何处理和准备,请查看
对每个状态的开发者建议
.
接下来的几个章节概括了这些新特性如何适应已经存在的web平台的状态和事件。
监听页面周期状态
在
active
,
passive
, 以及
hidden
这些状态时,可以从现在的web平台API中执行一些JavaScript代码判断当前页面生命周期状态。
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};
但
frozen
和
terminated
状态,当状态改变时只能在相应的事件(
freeze
和
pagehide
) 中才能监听到。
观察状态改变
基于上面定义的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;
}
};
// 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});
以上代码做了三件事:
警告!
这段代码在不同的浏览器中会产生不同的结果, 因为事件顺序(以及可靠性)实现还未统一。查看
管理跨浏览器差异
学习处理这些差异的最佳实践.
对上面的代码要提醒一点, 所有的事件监听器都要被添加到window而且他们都会传入
{capture: true}
. 这么做有几个原因:
-
不是所有的生命周期事件的target都一致。pagehide和pageshow事件会在window上触发; visibilitychange、freeze以及resume事件在document上触发, 而focus和blur事件会在他们各自的DOM元素上触发。
-
这些事件大部分不会冒泡,这就意味着不可能在公共的祖先上添加非捕获事件监听器监听所有的事件
-
捕获阶段在目标或冒泡阶段之前执行, 所以在此时添加事件监听器能确保它能在其他代码取消它们前执行。
管理跨浏览器差异
本篇文章的开始根据页面生命周期API概述了状态和事件流。但由于这些API刚被引入, 新的事件和DOM API还未在所有的浏览器中实现。
此外, 当下所有浏览器实现的事件也并未一致。例如:
-
当切换标签页时有些浏览器灭有触发blur事件。这就意味着(跟上面在表格和图表中的相反)页面可以直接从active 状态直接进入到hidden状态而不会先转为passive状态。
-
个别浏览器实现了
页面导航缓存
, 而页面生命周期API定义了缓存的页面应处于冻结状态。由于API完全是新的,所以这些浏览器还未实现freeze和resume事件, 即使这些状态仍然可以通过pagehide和pageshow事件被监听到。
-
IE浏览器老版(10及以下版本)没有实现visibilitychange事件。
-
pagehide和visibilitychange事件的发生顺序有所
改变
. 如果在卸载页面时,并且页面处于可见状态,早期浏览器会先触发pagehide事件再触发visibilitychange事件。新的Chrome版本则是先触发visibilitychange事件再触发pagehide事件,无论卸载时文档是否为可见状态。
为了让开发者更容易处理这些跨浏览器的矛盾问题,并能全心关注
生命周期状态建议以及最佳实践
, 我们发布了
PageLifecycle.js
,
这是个用来监听页面生命周期API状态改变的JavaScript库。
PageLifecycle.js
规范了跨浏览器在事件触发顺序的差异,这样状态就能准确如本文图表及表格中所述变化(而且在所有的浏览器中都能保持一致).
各种状态时对开发者的建议
作为开发者,了解页面生命周期_以及_知道在代码中如何监听它们都同样重要,因为你接下来应该做的(不应该做的)工作都会极大的依赖于当前页面的状态。
例如,如果页面处于hidden状态,很明显给用户瞬时性通知没有意义。由于这个例子非常明显,但总有不那么明显的场景,以下例举了其中值得关注的场景的建议.
对用户来说
active
状态是最关键的,因此这是最好的时间
响应用户输入
.
任何可能阻止主线程的非UI工作都应该被重新划分为
空闲时段
或
卸载到Web worker
.
| |
Passive
|
在
passive
状态时, 用户不会与页面互动,但仍对用户可见。这也就是说UI更新及动画依然会流畅,但这些更新时间就没那么重要了。
当页面从
active
更改为
passive
时,现在是保持未保存的应用程序状态的好时机。
| |
Hidden
|
当页面从
passive
状态切换到
hidden
状态时,可能用户不会再跟它有交互直到页面被重新加载.
开发者能可靠的监听到最后状态变化可能是页面转换到
hidden
状态。 (特别是在移动设备上, 因为用户可以关掉tab标签或者是浏览器,此时, beforeunload, pagehide, unload事件都不会触发).
这意味着您应该将
hidden
状态视为用户会话可能结束. 换言之,这时该保存所有没有保存的应用状态、发送还未发送的所有需要分析的数据。
这时候也应该停止更新UI(因为用户都不会看到了),也要关闭所有用户不希望在后台运行的程序。
| |
Frozen
|
页面处于
frozen
状态时,在
任务队列
冻结的任务
会被挂起直到页面不再被冻结——这可能永远不会发生(例如: 页面被抛弃).
这意味着页面从
hidden
转为
frozen
时,必须停止任何计时器或拆除任何连接,如果冻结,可能会影响同一源的其他open的选项卡标签,或影响浏览器将页面放入
页面导航缓存
的能力.
特别值得一提的是:
应该将任何动态的视图状态(e.g. 无限滚动列表视图的滚动位置) 保存到
sessionStorage
(或者
通过commit()提交到IndexedDB
) 这样的话,如果随后页面被抛弃又重新加载就能恢复原来的状态。
如果页面从
frozen
转变为
hidden
,你可以重新打开在任何开始冻结状态时关闭的连接或者重启那时被停止的轮询。
| |
Terminated
|
通常在页面转为
terminated
状态时不需要做任何操作。
由于页面即将被卸载,导致用户行为总是在进入terminated状态前进入hidden状态,进入hidden状态时应该执行会话结束逻辑(例如,保存应用程序状态和提交分析数据).