Web 页面生命周期 LifeCycle API

2,406 阅读8分钟

为什么 web 需要 LifeCycle API?

web上之前没有生命周期的概念,应用程序可以无限期地保持活力。

但是为了节省资源,浏览器需要采取积极措施来节省后台标签页面的资源,但是开发人员目前无法为这些类型的系统启动干预做好准备,浏览器需要保守或冒险破坏网页。

浏览器的判断总是不够精准的,为了解决这个问题,就引入了页面生命周期的 API。

LifeCycle API 包括什么?

  • 引入标准化生命周期的概念
  • 定义新的系统启动状态,允许浏览器限制隐藏或非活动选项卡可以使用的资源
  • 创建新的API和事件,允许Web开发人员响应这些新的系统启动状态的转换

chrome 68 引入

阶段介绍

网页的生命周期分为六个阶段:

Active

网页处理可见状态,且拥有输入焦点

Passive

网页可见,没有输入焦点,无法接受输入。UI更新(比如动画)仍在执行。只能发生在桌面同时有多个窗口的情况

Hidden

网页不可见,但尚未冻结。UI更新不再执行

Terminated

由于用户主动关闭窗口,或在同一个窗口前往其他页面,导致当前页面开始被浏览器卸载并从内存中清除。

  • 这个阶段总是在 Hidden 阶段之后发生,也就是说,用户主动离开当前页面,总是先进入 Hidden 阶段,再进去 Terminated 阶段
  • 这个节点会导致网页卸载,任何新任务都不会在这个阶段启动,并且如果运行时间太长,正在进行的任务可能会被终止

Frozen

如果网页处于 Hidden 阶段的时间过久,用户又不关闭网页,浏览器就有可能冻结网页,使其进入Frozen阶段。不过也有可能,处于可见状态的页面长时间没有操作,也会进入 Frozen 阶段

这个阶段中,网页不会再被分配 CPU 计算资源。定时器、回调函数、网络请求、DOM 操作都不会执行,不过正在运行的任务会执行完。浏览器可能会允许 Frozen 阶段的页面,周期性复苏一小段时间,短暂变回 Hidden 状态,允许一小部分任务执行

Discarded

如果网页长时间处理 Frozen 阶段,用户又不唤醒页面,就会进入 Discarded 阶段,即浏览器自动卸载网页,清除该网页的内存占用。不过,Passive 阶段的网页如果长时间没有互动,有可能直接进入 Discarded 阶段

一般在用户没有介入的情况下,由系统强制执行。任何类型的新任务或 JS 代码,都不能在此阶段执行,因为这时通常处于资源限制的状况下。

网页被浏览器自动 Discarded 以后,它的 Tab 窗口还是在的。如果用户重新访问这个 Tab 页,浏览器将会重新向服务器发出请求,再一次重新加载网页,回到 Active 阶段

事件说明

生命周期事件在所有帧(frame)触发。内嵌的 <iframe> 网页跟顶层网页一样,都会同时监听到下面的事件。

  • 事件处理程序要添加到 window 对象
  • visibilitychange 可以在 document 上监听;
  • freeze/resume 只能在 document 上监听

focus:

  • 页面获得输入焦点时触发
  • 比如 Passive -> Active

blur:

  • 页面失去输入焦点时触发
  • 比如 Active -> Passive
  • 事件处理程序要添加到 window 对象

visibilitychange

在网页可见状态发生变化时触发

  • <iframe> 的可见性状态与父 document 相同。
  • 使用 CSS 设置 <iframe> 不可见(如 display: none;)不会触发可见性事件或改变框架中包含文档的状态

比如:

  • 隐藏页面(切换 Tab,最小化浏览器等)Active -> Hidden
  • 用户重新访问隐藏的页面, Hidden -> Active
  • 用户关闭页面, Hidden -> Terminated

document.visibilityState 只读属性改变时,窗口状态为:

  • ‘visible’:
    • 页面内容至少部分可见。意味着页面是非最小化窗口的前景选项卡
  • ‘hidden’:
    • 后台选项卡 | 最小化窗口的一部分 | OS屏幕锁定
  • ‘prerender’
    • 页面内容正在预渲染且对用户不可见(document.hidden 视为 hidden)。
    • 文档可能在这个状态下启动,但不会由其他值转换得到
    • 浏览器可能不支持

  • 'unloaded'
    • 页面正从内存中卸载
    • 浏览器可能不支持

document.hidden只读属性:

  • true:页面对用户隐藏
  • false:其他
  • 由于历史原因保留,尽量使用 document.visibilityState 属性

freeze(新定义)

  • 在网页进入 Frozen 阶段时触发
  • 可以通过 document.onfreeze 属性指定进入 Frozen 阶段时调用的回调函数
  • 这个函数的监听事件,最长只能运行 500ms。并且只能复用已经打开的网络连接,不能发起新的网络请求
  • FrozenDiscarded 阶段不会触发任何事件,无法指定回调函数,只能在进入 Frozen 阶段时指定回调函数

resume(新定义)

  • resume 事件在网页离开 Frozen 阶段,变为 Active/Passive/Hidden阶段时触发
  • document.onresume 是指页面离开 Frozen 阶段,进入可用状态时调用的回调函数

pageshow:

  • 在用户加载网页时触发。这时可能是全新的页面加载,也可能是从缓存中获取的页面。如果是从缓存中获取,则该事件对象的 event.persisted 属性为 true,否则为 false

名字误导,其实与页面可见性无关,只跟浏览器的 History 记录的变化有关

pagehide:

在用户离开当前网页,进入另一个网页时触发。前提是浏览器的 History 记录必须发生变化,跟网页是否可见无关

如果浏览器能够将当前页面添加到缓存以供稍后重用,则事件对象的 event.persisted 属性为 true。如果页面添加到了缓存,则页面进入 Frozen 状态,否则进入 Terminatied 状态

beforeunload 事件

  • 在窗口或文档即将卸载时触发。该事件发生时,文档仍然可见,此时卸载仍可取消。经过这个事件,网页进入 Terminated 状态
  • 类似 unload 的问题。建议只在用户有未保存的更改时监听,在保存后立刻删除

unload 事件

  • 在页面正在卸载时触发。经过这个事件,网页进入 Terminated 状态
  • 现代浏览器中避免使用,尤其是移动设备上
  • 最好依靠 visibilitychange 事件来确定会话何时结束,并将 hidden 状态视为保存应用和用户数据的最后可靠时间
  • 建议使用 pagehide 事件来检测可能的页面卸载(terminated 状态)

试了下发现不同的事件需要绑在不同的对象上才能触发。visibilitychange 事件可以在 window/document 上触发,pagehide/pageshow/blur/focus 只能在 window 上触发,freezeresume 可以在 document 上触发。原因有待求证www.w3.org/TR/html/ful…

获取当前阶段的方法:

API

Active/Passive/Hidden 阶段可以通过下面代码获取状态:

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

事件监听

FrozenTerminated 状态,定时器代码不会执行,只能通过事件监听判断状态。

  • freeze: 进入Frozen
  • pagehide: Terminated

document.wasDiscarded (Chrome 68)

如果某个选项卡处于 Frozen 阶段,就随时有可能被系统丢弃,进入 Discarded 阶段,如果后来用户再次点击该选项卡,浏览器会重新加载该页面

这时,开发者可以通过判断 document.wasDiscarded 属性,了解先前的网页是否被丢弃了

同时,window 对象上会新增 window.clientIdwindow.discardedClientId 两个属性,用来恢复丢弃前的状态

跨浏览器差异

  • 尚未在所有浏览器中实现新事件和 DOM API

  • 现在在浏览器中实现的事件也是不一致的:

  • 兼容库: PageLifecycle.js

一些UA对后台或隐藏选项卡的限制策略:

多数浏览器停止向隐藏选项卡或隐藏 的 <iframe> 标签发送 requestAnimationFrame() 回调

例如 setTimeout() 的定时器在后台/非活动选项卡中受限,见 Reasons for delays longer than specified

现代浏览器(Firefox 58+, Chrome 57+)使用基于预算(budget-based)的后台超时限制,对后台CPU用量设置了额外限制,细节:

  • Firefox 中,背景选项卡中的窗口都有自己的时间预算(以毫秒为单位),最大值和最小值分别为 +50ms 和 -150ms。 Chrome 非常类似,只是预算以秒为单位指定

  • Windows 在 30s 后受到限制,具有与窗口计算器相同的限制延迟规则。在 Chrome 中,此值为10s

  • 只有在预算非负时,才允许定时器任务

  • 一旦定时器代码运行完毕,从其窗口的超时预算中减去执行所花费的持续时间

  • Firefox 和 Chrome 中,预算以每秒 10ms 的速度重新生成

某些进程可以免除此限制行为。这种情况下可以适应Page Visibility API在隐藏标签时降低标签的性能影响

  • 播放音频的选项卡被视为前景,不受影响
  • 运行实时网络连接(WebSockets 和 WebRTC)的代码的选项卡不受限制,以避免关闭这些连接超时并意外关闭
  • IndexDB 进程不受限制,以避免超时

chrome://discards/ 可以看到页面state并控制

兼容库 PageLifecycle.js

PageLifecycle.js实现:

getCurrentState()

  • 返回:active | passive | hidden
  • 根据文档可见性和是否有hasFocus()判断。其他状态需要通过getState()用监听事件的方法拿到

getState():

  • EVENTS:
    • IE9-10不支持pagehide,用unload代替
    • safari在关闭tab时不会可靠触发pagehide或visibilitychange,所以要用beforeunload和timeout定时来检查默认动作是否被阻止

参考:

标准: wicg.github.io/page-lifecy…

docs.google.com/document/d/…

chrome API: developers.google.com/web/updates…

阮一峰教程: www.ruanyifeng.com/blog/2018/1…

google 兼容库 PageLifecycle.js: github.com/GoogleChrom…