什么是 Page Lifecycle API?了解网页的生命周期

Page Lifecycle API 是由 WICG ( 不是 W3C ) 制定的标准,它统一了网页生命周期内的行为模式。本文介绍的是生命周期的状态,以及这些状态之间转换的事件。

网页的生命周期有哪些状态

网页的生命周期有 6 种状态,每个时刻只能处于其中一个状态。

  • Active:网页可见,拥有输入焦点

  • Passive:网页可见,没有输入焦点

  • Hidden:网页不可见,但未进入 frozen 等状态

  • Frozen:网页冻结

    Frozen 状态下,网页不会再被分配 CPU 计算资源。定时器、回调函数、网络请求、DOM 操作都不会执行,不过正在运行的任务会执行完。

    浏览器允许 Frozen 的页面周期性复苏一小段时间,短暂变回 Hidden 状态,允许一小部分任务执行。

    freeze 是保护 CPU、电池一种方式,也是实现更快的前后导航的一种方式,避免重新加载整个页面。

  • Discarded

    在资源紧张的情况下,浏览器为了节省资源而卸载页面,进入 Discarded 状态。

    这个状态下,不能运行任何任务、事件回调或 JavaScript 代码。

  • Terminated

    用户主动关闭窗口,或者超链接到其他页面,浏览器会从内存中清除当前页面,进入 Terminated 状态。

    这种状态下不能启动新任务,正在进行的任务如果运行时间过长可能会被终止。

生命周期的事件

网页的生命周期内,状态的变化都会触发相应的事件,供开发者指定监听函数。

  • focus:页面获得输入焦点时触发

  • blur:页面失去焦点时触发

  • visibilitychange

    页面可见状态变化时触发。比如用户导航到新页面、切换或关闭 tab、最小化或关闭浏览器、在移动端切换应用。

  • freeze

    页面进入 frozen 状态时触发。

    freeze 的监听函数,最长只能运行500毫秒,且只能复用已经打开的网络连接,不能发起新的网络请求。

    从 Frozen 到 Discarded,不会触发任何事件,因此无法指定回调函数,只能在进入 Frozen 时指定回调函数。

  • resume:页面从 frozen 变为 active、passive hidden 状态时触发

  • pageshow

    加载网页时触发。加载方式可以是全新加载,或者是从 bfcache 恢复页面。从 bfcache 恢复时,event.persisted 为 true。

  • pagehide

    用户离开当前页面,进入另一个页面时触发。

    如果页面满足 bfcache 的条件,event.persisted 为true,页面进入 frozen 状态,否则进入 terminated 状态。

  • beforeunload

    窗口或文档即将卸载时触发。此时文档仍可见,卸载仍可取消,经过这个事件,页面进入 Terminated 状态

  • unload

    页面正在卸载时触发。经过这个事件,页面进入 Terminated 状态。

注意,网页的生命周期事件是在所有帧 ( frame ) 触发,不管是底层的帧,还是内嵌的帧。也就是说,内嵌的 iframe 网页跟顶层网页一样,都会同时监听到下面的事件。

freeze 和 resume

以上 9 种页面生命周期事件中,只有 freeze 和 resume 是新标准定义的。

现在的浏览器会 freeze ( 冻结 ) 和 discard ( 丢弃 ) 隐藏的标签页,且时机由浏览器自行决定,开发人员无法知道何时会发生这种情况。

对于 freeze,可以通过监听文档上的 freeze 和 resume 事件来观察页面何时被冻结和解冻。

javascript 复制代码
document.addEventListener('freeze', (event) => {
  // The page is now frozen.
})

document.addEventListener('resume', (event) => {
  // The page has been unfrozen.
})

Discarded

对于 discard,可以通过以下代码判断页面是否曾被丢弃。

javascript 复制代码
if (document.wasDiscarded) {
  // Page was previously discarded by the browser while in a hidden tab.
}

另外,在 chrome 中输入地址 chrome://discards 可以观察当前所有 tag 页面的状态。

观察页面状态

如何获取当前的网页状态

当页面处于 Active、Passive 或 Hidden 状态时,可以直接获得当前页面状态。

javascript 复制代码
function getState() {
  if (document.visibilityState === 'hidden') return 'hidden'
  if (document.hasFocus()) return 'active'
  return 'passive'
}

Frozen 和 Terminated 状态只能在各自的事件监听器 ( freeze 和 pagehide ) 中检测到,因为状态正在发生变化。

javascript 复制代码
document.addEventListener('freeze', (event) => {
  // The page is now frozen.
})

document.addEventListener('pagehide', (event) => {
  // The page has been terminated.
})

Discarded 状态无法被获取,只能判断当前页面是否曾被丢弃。

javascript 复制代码
if (document.wasDiscarded) {
  // Page was previously discarded by the browser while in a hidden tab.
}

通过事件观察状态变化

基于 getState 函数,我们来编写观察所有状态变化的代码。

用 logStateChange 判断新旧状态是否改变。

javascript 复制代码
let state = getState()

function logStateChange(nextState) {
  const prevState = state
  if (nextState !== prevState) {
    console.log(`${prevState} => ${nextState}`)
    state = nextState
  }
}

监听页面生命周期事件。

javascript 复制代码
const events = ['pageshow', 'focus', 'blur', 'visibilitychange', 'resume']

events.forEach(type => {
  window.addEventListener(
    type,
    () => logStateChange(getState()),
    { capture: true }
  )
})

用 freeze 和 pagehide 事件观察 frozen 和 terminated 状态。

javascript 复制代码
window.addEventListener('freeze', () => {
  logStateChange('frozen')
}, { capture: true })
javascript 复制代码
window.addEventListener('pagehide', event => {
  logStateChange(event.persisted ? 'frozen' : 'terminated')
}, { capture: true })

以上所有的 addEventListener 都是在捕获阶段获取的,这是因为并非所有事件都有相同的 target。

  • pagehide、pageshow 在 window 上触发
  • visibilitychange、freeze、resume 在 document 上触发
  • focus 和 blur 在 DOM 元素上触发

这些事件大多不会冒泡,因此不可能通过一个共同的祖先元素观察所有事件,只能在捕获阶段获取 ( 捕获阶段在目标或冒泡阶段之前执行 )。

应避免使用的事件

unload 事件

不要在现代浏览器中使用 unload 事件。

许多开发者将 unload 事件作为会话结束信号来保存状态和发送分析数据,这样做非常不可靠。在许多情况下,unload 事件不会触发,比如在移动设备上关闭 tab 或退出浏览器。

此外,注册 unload 事件处理也会使得页面失去进入 bfcache 的资格。

因此,最好用 visibilitychange 事件来确定会话何时结束,并将 Hidden 状态视为保存数据的最后可靠时间 。在现代浏览器中,建议使用 pagehide 事件来检测页面的卸载。

beforeunload 事件

与 unload 类似,当 beforeunload 事件出现时,浏览器无法在 bfcache 中缓存页面。

建议当您想警告用户,退出页面将丢失未保存的更改时,可以使用 beforeunload 事件,并且在未保存的更改被保存后移除监听器。

javascript 复制代码
function beforeUnloadListener(event) {
  event.preventDefault()
  return (event.returnValue = 'Are you sure you want to exit?')
}

onPageHasUnsavedChanges(() => {
  addEventListener('beforeunload', beforeUnloadListener)
})

onAllChangesSaved(() => {
  removeEventListener('beforeunload', beforeUnloadListener)
})

针对页面状态的建议

  • Active

    Active 是页面响应用户输入的最重要时间,阻塞主线程的任务可以用 Web Worker 执行。

  • Passive

    Passive 状态下,用户不与页面交互,但仍可以看到页面,因此页面更新和动画仍应流畅。

    从 Active 变为 Passive 时,是保存应用程序状态的好时机

  • Hidden

    页面从 Passive 变为 Hidden 时,用户可能不会再与之交互,直到重新加载。

    向 Hidden 的转换通常也是开发人员能够可靠观察到的最后一次状态变化。在移动设备上尤其如此,用户关闭 tab 或浏览器,都不会触发 beforeunload、pagehide、unload 事件。

    因此,应将 Hidden 视为用户会话已经结束。换句话说,此时应该:

    • 持久化任何未保存的应用状态,发送分析数据
    • 停止进行 UI 更新 ( 反正用户看不到 )
    • 停止不必要的后台任务
  • Frozen

    在 Frozen 状态下,任务队列中的任务会被暂停。

    因此当页面从 Hidden 变为 Frozen 时,应停止定时器并关闭所有连接。

    • 关闭所有打开的 IndexedDB 连接。
    • 关闭打开的 BroadcastChannel 连接。
    • 关闭活动的 WebRTC 连接。
    • 停止网络轮询,关闭 Web Socket 连接。
    • 释放 Web 锁。

    此外,还应将视图状态,如列表的滚动位置,持久化到 sessionStorage 或 IndexedDB 中。

  • Terminated

    页面变为 Terminated 时,通常不需要采取任何操作。

    因为用户主动离开的页面在进入 Terminated 之前总要经过 Hidden 状态,Hidden 才是会话结束逻辑 ( 例如保存应用状态和发送分析报告 ) 执行的时机。

  • Discarded

    页面被 discard ( 丢弃 ) 时,开发者无法观察到丢弃状态。因为页面通常是在资源有限的情况下才被丢弃的,为了让脚本响应丢弃事件而解冻页面是不可能的。

    因此,您应该在 Hidden 变化为 Frozen 时提前做好准备,在页面加载时检查 document.wasDiscarded 对丢弃页面的恢复做出反应。

总结一下。

  • 页面处于 Active 时,应尽量使页面能快速响应用户
  • 从 Active 变为 Passive 时,应保存应用状态
  • 变为 Hidden 时,应视为用户会话已经结束,保存状态、发送分析数据等工作要尽快完成
  • Hidden 之后可能会变成 Frozen、Discarded、Terminated 等,这几个状态不太受开发者控制,都不适合执行会话结束逻辑。

安全的 freeze 和 discard

网页在 Hidden 状态下运行但不应该被冻结的情况有很多,最明显的例子就是播放音乐。

目前,Chrome 对丢弃是保守的,只有在确信不会对用户造成影响时才会丢弃页面。如果页面在 Hidden 下执行以下操作,则不会丢弃,除非资源极度紧张:

  • 播放音频
  • 使用WebRTC
  • 更新表格标题或图标
  • 显示提醒
  • 发送推送通知

关于如何确定是否安全冻结或丢弃,请参阅:Chrome 冻结和丢弃的启发式方法

参考

Page Lifecycle API

Page Lifecycle API 教程

相关推荐
好开心3313 分钟前
axios的使用
开发语言·前端·javascript·前端框架·html
百万蹄蹄向前冲2 小时前
2024不一样的VUE3期末考查
前端·javascript·程序员
alikami2 小时前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
wakangda3 小时前
React Native 集成原生Android功能
javascript·react native·react.js
吃杠碰小鸡3 小时前
lodash常用函数
前端·javascript
emoji1111113 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
一个处女座的程序猿O(∩_∩)O3 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
User_undefined3 小时前
uniapp Native.js原生arr插件服务发送广播到uniapp页面中
android·javascript·uni-app
麦兜*3 小时前
轮播图带详情插件、uniApp插件
前端·javascript·uni-app·vue
陈大爷(有低保)3 小时前
uniapp小案例---趣味打字坤
前端·javascript·vue.js