浏览器支持
如今,现代浏览器有时会暂停或彻底舍弃网页, 系统资源受到限制。将来,浏览器也希望 降低功耗和内存Page Lifecycle API 提供生命周期钩子,以便您的网页可以安全地处理这些浏览器, 干预,同时又不影响用户体验。查看 API 看看是否应该在应用中实现这些功能。
背景
应用生命周期是现代操作系统管理应用生命周期 资源。在 Android、iOS 和最近的 Windows 版本中, 操作系统会随时停止运行这使得这些平台能够简化和 将资源重新分配对用户最有利的资源。
在网站上,向来没有这样的生命周期,应用可以保留 那就是永恒的生命运行大量网页时,关键系统 内存、CPU、电池和网络等资源可能会被过度订阅, 进而导致最终用户体验不佳
长期以来,Web 平台都会出现与生命周期状态相关的事件
— 例如 load
,
unload
和
visibilitychange
这些事件只允许开发者
来响应用户发起的生命周期状态变化。在网页上高效工作
在低功耗设备上可靠地运行(并且通常在
所有平台)的浏览器需要一种方法来主动回收和重新分配系统
资源。
事实上,当今的浏览器已采取积极措施来节约资源 ,而许多浏览器(尤其是 Chrome)都希望 可以做更多的事情,以减少其总体资源配额。
但问题是,开发者没有办法 系统启动的干预措施,甚至是系统启动的干预措施。这意味着 浏览器需要比较保守,否则可能导致网页无法正常运作。
Page Lifecycle API 尝试通过以下方式解决此问题:
- 在网络上引入生命周期状态的概念并对其进行标准化。
- 定义新的由系统启动的状态,允许浏览器限制 可供隐藏或不活跃标签页使用的资源。
- 创建新的 API 和事件,使 Web 开发者能够响应 过渡到和从这些新的系统启动状态过渡。
此解决方案提供了 Web 开发者在构建 让浏览器能够更好地应对系统干预 积极优化系统资源,最终使所有网络用户受益。
本博文的其余部分将介绍页面生命周期中的新功能 并探索它们与所有现有网络平台状态的关系 和事件。还会针对不同类型 开发者应该(以及不应)在每种状态下执行的工作。
页面生命周期状态和事件概览
所有页面生命周期状态都是离散且互斥的,这意味着一个页面 一次只能处于一种状态而对页面的生命周期状态所做的大多数更改 通常可通过 DOM 事件观察到(有关例外情况,请参阅针对每种状态的开发者建议)。
解释页面生命周期状态以及 下面用一张图来说明这些事件之间的转换信号:
州
下表详细介绍了每种状态。它还列出了 之前和之后可能发生的状态,以及开发者可以 来观察更改
州 | 说明 |
---|---|
有效 |
如果某个页面可见且处于活跃状态,则处于活动状态。 输入焦点。
之前可能显示的状态: |
被动 |
如果某个页面可见且确实处于被动状态, 没有输入焦点。
之前可能显示的状态:
接下来可能会出现的状态: |
已隐藏 |
如果某个网页不可见(也未展示),则处于隐藏状态。 已冻结、舍弃或终止)。
之前可能显示的状态:
接下来可能会出现的状态: |
已冻结 |
在已冻结状态下,浏览器会暂停
<ph type="x-smartling-placeholder"></ph>
可冻结
任务
任务队列,直到页面取消冻结。也就是说,
JavaScript 计时器和提取回调不运行。已在运行
(最重要的是,
浏览器冻结网页,以保持 CPU/电池/数据流量消耗;他们 也可以让它更快地 往返导航 - 无需整页显示 重新加载。
接下来可能会出现的状态: |
已终止 |
网页一旦开始播放,就会处于已终止状态 由浏览器卸载并从内存中清除。否 新任务都可以在此状态下启动,正在进行的任务可能会 会遭到终止
之前可能显示的状态:
接下来可能会出现的状态: |
已舍弃 |
页面在被 以便节省资源无任务、事件回调或 任何类型的 JavaScript 都可以在此状态下运行,因为丢弃通常是 发生在资源限制下,此时启动新进程 不可能 处于已舍弃状态的标签页本身 (包括标签页标题和网站图标)通常对用户可见 即使该网页已不复存在。
之前可能显示的状态:
接下来可能会出现的状态: |
事件
浏览器会分派大量事件,但其中只有一小部分事件 页面生命周期状态的可能变化。下表列出了所有事件 与生命周期有关,并列出了它们可能过渡到哪些状态以及从哪些状态过渡。
名称 | 详细信息 |
---|---|
focus
|
某个 DOM 元素已获得焦点。
注意:
之前可能显示的状态:
当前可能的状态: |
blur
|
某个 DOM 元素失去了焦点。
注意:
之前可能显示的状态:
当前可能的状态: |
visibilitychange
|
文档的
|
freeze
*
|
该网页刚刚冻结了。不限 freezable 任务将不会启动。
之前可能显示的状态:
当前可能的状态: |
resume
*
|
浏览器恢复了冻结网页。
之前可能显示的状态:
当前可能的状态: |
pageshow
|
正在遍历会话历史记录条目。 这可能是全新的网页加载,也可能是
往返缓存。如果网页
来自往返缓存,则事件的
|
pagehide
|
正在遍历会话历史记录条目。 如果用户要前往其他网页,并且浏览器能够添加
从当前网页跳转到后退/前进
cache 以供日后重复使用,即事件的
之前可能显示的状态:
当前可能的状态: |
beforeunload
|
窗口、文档及其资源即将卸载。 该文档仍然可见,并且此活动仍可以在此时取消 。
重要提示:
之前可能显示的状态:
当前可能的状态: |
unload
|
正在卸载该网页。
警告:
我们不建议使用
之前可能显示的状态:
当前可能的状态: |
* 表示由 Page Lifecycle API 定义的新事件
Chrome 68 中增加的新功能
上图显示了两种状态,它们由系统启动,而不是由系统启动, user-initiated: frozen(已冻结)和 discarded(已舍弃)。 如前所述,目前的浏览器已偶尔 隐藏标签页,但开发者无从得知 情况就是如此
在 Chrome 68 中,开发者现在可以观察隐藏的标签页何时冻结,
通过监听 freeze
解除冻结
以及在 document
上发生的 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.
}
获取关于freeze
和resume
中重要事项的建议
事件,以及如何处理和处理被舍弃的网页,请参阅
针对每种状态的开发者建议。
接下来的几个部分将简要介绍这些新功能如何 现有的网络平台状态和事件。
如何在代码中观察页面生命周期状态
在主动、被动和隐藏中 可运行 JavaScript 代码来确定当前 现有网络平台 API 中的页面生命周期状态。
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};
已冻结和已终止状态
只能在相应的事件监听器中检测到
(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;
}
};
// 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}
。
导致这种情况的原因有以下几种:
- 并非所有页面生命周期事件都有相同的目标。
pagehide
和 对window
触发了pageshow
;visibilitychange
、freeze
和resume
针对document
触发,而focus
和blur
针对其触发 相应的 DOM 元素。 - 其中大多数活动都不会以消息气泡形式显示,这意味着您无法添加 非捕获事件监听器监听共同的祖先元素,并观察所有 。
- 拍摄阶段在目标阶段或气泡阶段之前执行,因此添加 该监听器中的监听器有助于确保它们在其他代码可以取消之前运行。
针对每种状态的开发者建议
作为开发者,了解页面生命周期状态以及 您知道如何在代码中观察它们,因为您应该(也应该) 这在很大程度上取决于您的页面所处的状态。
例如,显示瞬时通知显然没有意义 如果页面处于隐藏状态,则会向用户显示。虽然这个示例 还有其他不太明显的建议 枚举。
州 | 开发者建议 |
---|---|
Active |
活跃状态是用户最关键的时段,因此 加载网页的最重要时间 <ph type="x-smartling-placeholder"></ph> 响应用户输入内容 应降低任何可能阻塞主线程的非界面工作的优先级 发送至 空闲时段或 分流到 Web Worker。 |
Passive |
在被动状态下,用户未与网页互动; 但他们仍然可以看到它。这意味着界面更新和动画仍应 但这些更新的发生时间则没那么重要。 当网页从“主动”变为“被动”时, 是保留未保存应用状态的好时机。 |
当网页从被动变为隐藏时, 用户在它重新加载之前可能不会再与它互动。 转换为隐藏状态通常也是最后一次状态更改
(特别是在 Google Play 上
因为用户可以关闭标签页或浏览器应用本身;
也就是说,您应将 hidden 状态视为 会话。换言之,保留任何未保存的应用状态 并发送任何未发送的分析数据。 您还应停止进行界面更新(因为它们将 用户),并且您应该停止任何用户不想执行的任务 以及它们在后台运行 |
|
Frozen |
在冻结状态下, <ph type="x-smartling-placeholder"></ph> 冻结任务 任务队列将一直处于暂停状态,直到页面取消冻结, 绝不会发生(例如,如果网页被舍弃)。 也就是说,当网页从“隐藏”变为“已冻结”时 请务必停止任何计时器,或断开任何 如果冻结,可能会影响同源中的其他打开的标签页,或者影响 浏览器将页面放入 往返缓存。 具体而言,请务必:
您还应保留所有动态视图状态(例如滚动位置)
)更改为
如果网页从“冻结”状态转换回“隐藏”状态,则: 你可以重新打开所有已关闭的连接,也可以重新开始 在网页最初冻结时停止。 |
Terminated |
当网页转换时,您通常无需执行任何操作 更改为 terminated 状态。 由于用户操作导致网页被卸载 在进入 terminated 之前,会经历 hidden 状态 状态,则隐藏状态是会话结束逻辑(例如, 应用状态并向分析报告) 错误。 此外(如针对
隐藏状态),开发者必须要认识到,
到“已终止”状态的转换无法可靠地完成
大多数情况下(尤其是在移动设备上)都会检测到这种技术,因此
(例如, |
Discarded |
discarded 状态无法由开发者在 网页被舍弃时触发。这是因为 以及由于资源限制而舍弃的网页 运行脚本来响应舍弃事件,这在 。 因此,您应为在
将状态从隐藏改为已冻结,然后您就可以
对在网页加载时恢复的舍弃网页做出反应,方法是使用
正在检查 |
再次强调,由于生命周期事件的可靠性和排序 在所有浏览器中始终如一地实施,那么遵循建议的最简单方式 使用 PageLifecycle.js。
要避免的旧版生命周期 API
应尽可能避免以下事件。
unload 事件
许多开发者将 unload
事件视为有保证的回调,并将其用作
保存状态并发送分析数据的会话结束信号,但这样做
非常不可靠,尤其是在移动设备上!unload
事件不会
在许多典型的卸载情况下都会触发,包括从标签页中关闭标签页
在移动设备上切换器或从应用切换器中关闭浏览器应用。
因此,最好使用
visibilitychange
事件,用于确定会话
将隐藏状态视为
节省应用和用户数据的可靠时间。
此外,仅存在一个已注册的 unload
事件处理脚本(通过
onunload
或 addEventListener()
)将会导致浏览器无法
将网页放入往返缓存,以加快速度
。
在所有新型浏览器中,建议始终使用
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
事件
进一步影响其可靠性。
beforeunload
和 unload
之间的一个区别在于,
对 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
事件处理脚本,则系统将阻止进行此类优化。
对于所有 intent 和目的,这种冻结在功能上与 冻结的浏览器为节省 CPU/电池电量因此 被视为已冻结生命周期状态的一部分。
如果我无法在冻结或终止状态下运行异步 API,如何将数据保存到 IndexedDB?
在冻结和终止状态下 可冻结的任务 在网页的任务队列中 会暂停,这意味着异步 API 和基于回调的 API,例如 IndexedDB 无法可靠地使用。
将来,我们将为 IDBTransaction
对象添加 commit()
方法,该方法将
让开发者能够执行有效的只写事务
不需要回调换句话说,如果开发者只是
将数据导出到 IndexedDB,并且未执行包含读取的复杂事务
和写入,commit()
方法能够在任务队列
已暂停(假设 IndexedDB 数据库已经打开)。
不过,对于需要立即运行的代码,开发者有两种选择:
- 使用会话存储空间:会话存储空间 是同步的,并且会在页面舍弃后持久保留。
- 使用 Service Worker 中的 IndexedDB:Service Worker 可以将数据存储在
在网页被终止或舍弃后编入索引的 IndexedDB。在
freeze
或pagehide
事件监听器,您可以通过以下代码向 Service Worker 发送数据:postMessage()
, Service Worker 可以负责保存数据。
在冻结和舍弃状态下测试应用
如需测试应用在冻结和舍弃状态下的行为,您可以访问
chrome://discards
即可冻结或舍弃
打开的标签页。
![Chrome 舍弃界面](https://developer.chrome.google.cn/static/docs/web-platform/page-lifecycle-api/image/chrome-discards-ui-4c0620d6f8619.png?authuser=8&hl=ca)
这样,您就可以确保您的网页正确处理 freeze
和 resume
。
事件以及 document.wasDiscarded
标记(当页面重新加载后
舍弃了。
摘要
希望尊重用户设备的系统资源的开发者 构建应用时应考虑到页面生命周期状态。需要注意的是 网页不会消耗过多的系统资源, 用户不会预料到的
开始实现新的 Page Lifecycle API 的开发者越多,就越安全 将使浏览器冻结并舍弃未使用的网页。本次 这意味着浏览器消耗的内存、CPU、电池和网络资源都会减少, 这对用户来说是一次胜利