页面生命周期 API

浏览器支持

  • 68
  • 79
  • x
  • x

如今,现代浏览器有时会暂停或彻底舍弃网页, 系统资源受到限制。将来,浏览器也希望 降低功耗和内存Page Lifecycle API 提供生命周期钩子,以便您的网页可以安全地处理这些浏览器, 进行干预,同时又不影响用户体验。查看 API 看看是否应该在应用中实现这些功能。

背景

应用生命周期是现代操作系统管理应用生命周期 资源。在 Android、iOS 和最近的 Windows 版本中, 被操作系统随时停止。这使得这些平台能够简化和 将资源重新分配对用户最有利的资源。

在网站上,向来没有这样的生命周期,应用可以保留 那就是永恒的生命运行大量网页时,关键系统 内存、CPU、电池和网络等资源可能会被过度订阅, 进而导致最终用户体验不佳

长期以来,Web 平台都会出现与生命周期状态相关的事件 — 例如 loadunloadvisibilitychange 这些事件只允许开发者 来响应用户发起的生命周期状态变化。在网页上高效工作 在低功耗设备上可靠地运行(并且通常在 所有平台)的浏览器需要一种方法来主动回收和重新分配系统 资源。

事实上,当今的浏览器已采取积极措施来节约资源 ,而许多浏览器(尤其是 Chrome)都希望 可以做更多的事情,以减少其总体资源配额。

但问题是,开发者没有办法 系统启动的干预措施,甚至是系统启动的干预措施。这意味着 浏览器需要比较保守,否则可能导致网页无法正常运作。

Page Lifecycle API 尝试通过以下方式解决此问题:

  • 在网络上引入生命周期状态的概念并对其进行标准化。
  • 定义新的由系统启动的状态,允许浏览器限制 可供隐藏或不活跃标签页使用的资源。
  • 创建新的 API 和事件,使 Web 开发者能够响应 过渡到和从这些新的系统启动状态过渡。

此解决方案提供了 Web 开发者在构建 让浏览器能够更好地应对系统干预 积极优化系统资源,最终使所有网络用户受益。

本博文的其余部分将介绍页面生命周期中的新功能 并探索它们与所有现有网络平台状态的关系 和事件。还会针对不同类型 开发者应该(以及不应)在每种状态下执行的工作。

页面生命周期状态和事件概览

所有页面生命周期状态都是离散且互斥的,这意味着一个页面 一次只能处于一种状态而对页面的生命周期状态所做的大多数更改 通常可通过 DOM 事件观察到(有关例外情况,请参阅针对每种状态的开发者建议)。

解释页面生命周期状态以及 下面用一张图来说明这些事件之间的转换信号:

<ph type="x-smartling-placeholder"></ph> 本文档通篇介绍的状态和事件流的直观表示。
Page Lifecycle API 状态和事件流

下表详细介绍了每种状态。它还列出了 之前和之后可能发生的状态,以及开发者可以 来观察更改

说明
有效

如果某个页面可见且处于活跃状态,则处于活动状态。 输入焦点。

之前可能显示的状态
被动 (通过 focus 事件)
已冻结 (通过 resume 事件,然后 pageshow 事件)

接下来可能会出现的状态
被动 (通过 blur 事件)

被动

如果某个页面可见且确实处于被动状态, 没有输入焦点。

之前可能显示的状态
有效 (通过 blur 事件)
已隐藏 (通过 visibilitychange 事件)
已冻结 (通过 resume 事件,然后 pageshow 事件)

接下来可能会出现的状态
有效 (通过 focus 事件)
已隐藏 (通过 visibilitychange 事件)

已隐藏

如果某个网页不可见(也未展示),则处于隐藏状态。 已冻结、舍弃或终止)。

之前可能显示的状态
被动 (通过 visibilitychange 事件)
已冻结 (通过 resume 事件,然后 pageshow 事件)

接下来可能会出现的状态
被动 (通过 visibilitychange 事件)
已冻结 (通过 freeze 事件)
已舍弃 (未触发任何事件)
已终止 (未触发任何事件)

已冻结

已冻结状态下,浏览器会暂停 可冻结 <ph type="x-smartling-placeholder"></ph> 任务 任务队列,直到页面取消冻结。也就是说, JavaScript 计时器和提取回调不运行。已在运行 (最重要的是, freeze 回调),但他们的操作可能会受到限制 可以执行的操作,以及可以运行多长时间。

浏览器冻结网页,以保持 CPU/电池/数据流量消耗;他们 也可以让它更快地 往返导航 - 无需整页显示 重新加载。

之前可能显示的状态
已隐藏 (通过 freeze 事件)

接下来可能会出现的状态
有效 (通过 resume 事件,然后 pageshow 事件)
被动 (通过 resume 事件,然后 pageshow 事件)
已隐藏 (通过 resume 事件)
已舍弃 (未触发任何事件)

已终止

网页一旦开始播放,就会处于已终止状态 由浏览器卸载并从内存中清除。否 新任务都可以在此状态下启动,正在进行的任务可能会 会遭到终止

之前可能显示的状态
已隐藏 (通过 pagehide 事件)

接下来可能会出现的状态

已舍弃

页面在被 以便节省资源无任务、事件回调或 任何类型的 JavaScript 都可以在此状态下运行,因为丢弃通常是 发生在资源限制下,此时启动新进程 不可能

处于已舍弃状态的标签页本身 (包括标签页标题和网站图标)通常对用户可见 即使该网页已不复存在。

之前可能显示的状态
已隐藏 (未触发任何事件)
已冻结 (未触发任何事件)

接下来可能会出现的状态

事件

浏览器会分派大量事件,但其中只有一小部分事件 页面生命周期状态的可能变化。下表列出了所有事件 与生命周期有关,并列出了它们可能过渡到哪些状态以及从哪些状态过渡。

名称 详细信息
focus

某个 DOM 元素已获得焦点。

注意focus 事件不会 必然会发出状态变化的信号只有在出现以下情况时,它才会指示状态发生变化: 此页面之前没有输入焦点。

之前可能显示的状态
被动

当前可能的状态
有效

blur

某个 DOM 元素失去了焦点。

注意blur 事件不会 必然会发出状态变化的信号只有在出现以下情况时,它才会指示状态发生变化: 页面不再具有输入焦点(即页面不仅仅切换了 将焦点从一个元素移到另一个元素)。

之前可能显示的状态
有效

当前可能的状态
被动

visibilitychange

文档的 visibilityState值已更改。这可以 当用户转到新页面、切换标签页、关闭标签页时, 最小化或关闭浏览器,或在移动设备运行时切换应用 系统。

之前可能显示的状态
被动
已隐藏

当前可能的状态
被动
已隐藏

freeze *

该网页刚刚冻结了。不限 freezable 任务将不会启动。

之前可能显示的状态
已隐藏

当前可能的状态
已冻结

resume *

浏览器恢复了冻结网页。

之前可能显示的状态
已冻结

当前可能的状态
有效 (如果后跟 pageshow 事件)
被动 (如果后跟 pageshow 事件)
已隐藏

pageshow

正在遍历会话历史记录条目。

这可能是全新的网页加载,也可能是 往返缓存。如果网页 来自往返缓存,则事件的 persisted 属性为 true,否则为 false

之前可能显示的状态
已冻结 resume) 事件也会被触发)

当前可能的状态
活跃
被动
已隐藏

pagehide

正在遍历会话历史记录条目。

如果用户要前往其他网页,并且浏览器能够添加 从当前网页跳转到后退/前进 cache 以供日后重复使用,即事件的 persisted 属性 为 true。如果值为 true,则表明网页正在进入 处于“已冻结”状态,否则会进入“已终止”状态。

之前可能显示的状态
已隐藏

当前可能的状态
已冻结 event.persisted 为 true, freeze 活动后续安排)
已终止 event.persisted 为 false, unload 事件关注)

beforeunload

窗口、文档及其资源即将卸载。 该文档仍然可见,并且此活动仍可以在此时取消 。

重要提示beforeunload 事件 应仅用于提醒用户有未保存的更改。这些 更改已保存,则应移除该事件。切勿 添加到网页中,因为这样做会影响 在某些情况下。请参阅旧版 “API”部分了解详情。

之前可能显示的状态
已隐藏

当前可能的状态
已终止

unload

正在卸载该网页。

警告: 我们不建议使用unload事件 不可靠,并且在某些情况下可能会影响性能。请参阅 旧版 API 部分 了解详情。

之前可能显示的状态
已隐藏

当前可能的状态
已终止

* 表示由 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.
}

获取关于freezeresume中重要事项的建议 事件,以及如何处理和处理被舍弃的网页,请参阅 针对每种状态的开发者建议

接下来的几个部分将简要介绍这些新功能如何 现有的网络平台状态和事件。

如何在代码中观察页面生命周期状态

主动被动隐藏中 可运行 JavaScript 代码来确定当前 现有网络平台 API 中的页面生命周期状态。

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

已冻结已终止状态 只能在相应的事件监听器中检测到 (freezepagehide),因为状态 变化。

如何观察状态变化

基于之前定义的 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 触发了 pageshowvisibilitychangefreezeresume 针对 document 触发,而 focusblur 针对其触发 相应的 DOM 元素。
  • 其中大多数活动都不会以消息气泡形式显示,这意味着您无法添加 非捕获事件监听器监听共同的祖先元素,并观察所有 。
  • 拍摄阶段在目标阶段或气泡阶段之前执行,因此添加 该监听器中的监听器有助于确保它们在其他代码可以取消之前运行。

针对每种状态的开发者建议

作为开发者,了解页面生命周期状态以及 您知道如何在代码中观察它们,因为您应该(也应该) 这在很大程度上取决于您的页面所处的状态。

例如,显示瞬时通知显然没有意义 如果页面处于隐藏状态,则会向用户显示。虽然这个示例 还有其他不太明显的建议 枚举。

开发者建议
Active

活跃状态是用户最关键的时段,因此 加载网页的最重要时间 响应用户输入内容

应降低任何可能阻塞主线程的非界面工作的优先级 发送至 空闲时段 分流到 Web Worker

Passive

在被动状态下,用户未与网页互动; 但他们仍然可以看到它。这意味着界面更新和动画仍应 但这些更新的发生时间则没那么重要。

当网页从“主动”变为“被动”时, 是保留未保存应用状态的好时机。

Hidden

当网页从被动变为隐藏时, 用户在它重新加载之前可能不会再与它互动。

转换为隐藏状态通常也是最后一次状态更改 (特别是在 Google Play 上 因为用户可以关闭标签页或浏览器应用本身; beforeunloadpagehideunload 事件时不会触发)。

也就是说,您应将 hidden 状态视为 会话。换言之,保留任何未保存的应用状态 并发送任何未发送的分析数据。

您还应停止进行界面更新(因为它们将 用户),并且您应该停止任何用户不想执行的任务 以及它们在后台运行

Frozen

冻结状态下, 冻结任务 <ph type="x-smartling-placeholder"></ph> 任务队列将一直处于暂停状态,直到页面取消冻结, 绝不会发生(例如,如果网页被舍弃)。

也就是说,当网页从“隐藏”变为“已冻结”时 请务必停止任何计时器,或断开任何 如果冻结,可能会影响同源中的其他打开的标签页,或者影响 浏览器将页面放入 往返缓存

具体而言,请务必:

您还应保留所有动态视图状态(例如滚动位置) )更改为 sessionStorage(或 IndexedDB 通过 commit()) 并在稍后重新加载。

如果网页从“冻结”状态转换回“隐藏”状态,则: 你可以重新打开所有已关闭的连接,也可以重新开始 在网页最初冻结时停止。

Terminated

当网页转换时,您通常无需执行任何操作 更改为 terminated 状态。

由于用户操作导致网页被卸载 在进入 terminated 之前,会经历 hidden 状态 状态,则隐藏状态是会话结束逻辑(例如, 应用状态并向分析报告) 错误。

此外(如针对 隐藏状态),开发者必须要认识到, 到“已终止”状态的转换无法可靠地完成 大多数情况下(尤其是在移动设备上)都会检测到这种技术,因此 (例如,beforeunloadpagehideunload)可能会丢失数据。

Discarded

discarded 状态无法由开发者在 网页被舍弃时触发。这是因为 以及由于资源限制而舍弃的网页 运行脚本来响应舍弃事件,这在 。

因此,您应为在 将状态从隐藏改为已冻结,然后您就可以 对在网页加载时恢复的舍弃网页做出反应,方法是使用 正在检查document.wasDiscarded

再次强调,由于生命周期事件的可靠性和排序 在所有浏览器中始终如一地实施,那么遵循建议的最简单方式 使用 PageLifecycle.js

要避免的旧版生命周期 API

应尽可能避免以下事件。

unload 事件

许多开发者将 unload 事件视为有保证的回调,并将其用作 保存状态并发送分析数据的会话结束信号,但这样做 非常不可靠,尤其是在移动设备上!unload 事件不会 在许多典型的卸载情况下都会触发,包括从标签页中关闭标签页 在移动设备上切换器或从应用切换器中关闭浏览器应用。

因此,最好使用 visibilitychange 事件,用于确定会话 将隐藏状态视为 节省应用和用户数据的可靠时间

此外,仅存在一个已注册的 unload 事件处理脚本(通过 onunloadaddEventListener())将会导致浏览器无法 将网页放入往返缓存,以加快速度 。

在所有新型浏览器中,建议始终使用 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 事件 进一步影响其可靠性。

beforeunloadunload 之间的一个区别在于, 对 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。在 freezepagehide 事件监听器,您可以通过以下代码向 Service Worker 发送数据: postMessage(), Service Worker 可以负责保存数据。

在冻结和舍弃状态下测试应用

如需测试应用在冻结和舍弃状态下的行为,您可以访问 chrome://discards即可冻结或舍弃 打开的标签页。

<ph type="x-smartling-placeholder"></ph> Chrome 舍弃界面
Chrome 舍弃界面

这样,您就可以确保您的网页正确处理 freezeresume。 事件以及 document.wasDiscarded 标记(当页面重新加载后 舍弃了。

摘要

希望尊重用户设备的系统资源的开发者 构建应用时应考虑到页面生命周期状态。需要注意的是 网页不会消耗过多的系统资源, 用户不会预料到的

开始实现新的 Page Lifecycle API 的开发者越多,就越安全 将使浏览器冻结并舍弃未使用的网页。这个 这意味着浏览器消耗的内存、CPU、电池和网络资源都会减少, 这对用户来说是一次胜利