一文打通浏览器页面生命周期

136 阅读14分钟

前言

我相信大家都做过这样一件事:打开浏览器输入某个网址(比如https://juejin.cn),然后回车进入网站浏览一番,最后心满意足地关闭页面。这样我们看来再正常不过的事情,其实包含了一个浏览器页面由生到死的全过程。本文又称“从输入URL到浏览器页面入土全过程”,下面我将为你介绍从用户触发导航(如输入URL、点击链接)到页面最终被关闭的全过程。

一. 页面生命周期概述

《黑神话:悟空》这款游戏中,作为天命人,我们要找到大圣的“六根”将其复活,而在浏览器页面中,同样也有“六根”,它们分别是ACTIVE(活跃)、PASSIVE(被动可见)、HIDDEN(隐藏)、TERMINATED(终止)、FROZEN(冻结)、DISCARDED(丢弃),是为浏览器页面的六个核心生命周期状态。

生命周期状态视图

原视图

生命周期视图.svg

翻译解析视图

  • 我没找到中文版的视图,所以自行翻译解析了一下,如有错误,请帮我指正。

浏览器页面生命周期视图.png

  • 注意:
    • 可靠事件: 浅黄色事件框代表的“可靠事件”,指的是浏览器稳定支持,触发时机可信赖,如 visibilitychange
    • 不可靠事件: 橙色事件框代表的“不可靠事件”,指的是触发时机不确定,或旧浏览器兼容性差,如早期 pagehide 行为

六大核心状态

  • 核心状态分类: 从图中可以看出,这六个核心状态分为用户交互驱动(蓝色,即为用户交互有关的状态)以及浏览器优化驱动(紫色,即为浏览器自发的优化行为有关的状态)两大类。
    • 用户交互驱动:
      • ACTIVE(活跃):页面可见且获得焦点,用户正在交互(用户正在当前标签页交互,如点击、输入、滚动)。
      • PASSIVE(被动可见):页面可见但失去焦点(如多窗口并列,焦点在其他窗口)
      • HIDDEN(隐藏):页面不可见(如切换到其他标签、最小化窗口),脚本仍可运行(需主动节流)
      • TERMINATED(终止):页面完全卸载(资源释放,无法恢复,如关闭标签页、导航到新页面)。
    • 浏览器优化驱动:
      • FROZEN(冻结):页面不可见且脚本暂停(通常为页面处于HIDDEN一段时间之后,浏览器主动冻结,保留 DOM/JS 状态,节省 CPU)。
      • DISCARDED(丢弃):页面状态被彻底清空(DOM、JS 上下文、变量全销毁,浏览器因内存压力丢弃)。
  • 核心状态总结:
状态定义进入条件(前状态 → 触发事件 / 行为)离开条件(后状态 → 触发事件 / 行为)
ACTIVE页面同时可见且获得焦点1️⃣ PASSIVE → 用户点击标签页(获焦),触发 focus 事件;2️⃣ pageshow → 页面加载后,document.hasFocus() === truePASSIVE → 用户切换到其他窗口 / 标签(失焦),触发 blur 事件
PASSIVE页面可见但失去焦点1️⃣ ACTIVE → 用户切换到其他窗口(失焦),触发 blur 事件;2️⃣ pageshow → 页面加载后,document.hasFocus() === false1️⃣ ACTIVE → 用户切回标签页(获焦),触发 focus 事件;2️⃣ HIDDEN → 用户切换到其他标签页(页面不可见),触发 visibilitychange 事件(document.visibilityState = 'hidden'
HIDDEN页面不可见,但脚本仍可运行1️⃣ PASSIVE → 用户切换标签页,触发 visibilitychange 事件(document.visibilityState = 'hidden');2️⃣ FROZEN → 用户切回冻结标签,触发 resume 事件1️⃣ PASSIVE → 用户切回标签页(页面可见),触发 visibilitychange 事件(document.visibilityState = 'visible')且未获焦(document.hasFocus() === false);2️⃣ FROZEN → 浏览器为省 CPU,触发 freeze 事件;3️⃣ TERMINATED → 触发 beforeunload(可取消)→ pagehideevent.persisted = false,不缓存)→ unload;4️⃣ DISCARDED → 浏览器内存严重不足,直接销毁页面状态
FROZEN页面不可见且脚本暂停HIDDENHIDDEN 状态下,浏览器判断 “可冻结”(如无实时数据需求),触发 freeze 事件1️⃣ HIDDEN → 用户切回冻结标签,触发 resume 事件;2️⃣ DISCARDED → 冻结后内存仍不足,浏览器强制清空状态
DISCARDED页面状态被彻底清空1️⃣ HIDDEN → 浏览器内存严重不足,静默丢弃;2️⃣ FROZEN → 冻结后内存仍不足,静默丢弃ACTIVE/PASSIVE(重新加载后)→ 用户切回丢弃标签,浏览器强制重新加载,触发 load/pageshowdocument.wasDiscarded = true,需重建状态),最终依焦点判断进入 ACTIVE 或 PASSIVE
TERMINATED页面完全卸载HIDDEN → 用户主动导航离开 / 关闭标签页,且 pagehide.persisted = false(不缓存)→ 触发 unload 事件无(已销毁,无法转换至其他状态)

二. 案例分析(从输入URL到页面入土全过程分析)

如果只是看上面的概念,我相信大多数人都无法理解这个知识点,毕竟“纸上得来终觉浅,绝知此事要躬行”,现在我就带大家来全面分析一下案例:“从输入juejin.cn并回车,到页面关闭全过程 ”,以案例说话。

1. 页面初始加载阶段(无状态 -> ACTIVE状态)

  • 用户操作:在浏览器地址栏输入 https://juejin.cn 并回车,浏览器开始解析 URL、建立连接、请求资源(HTML/CSS/JS 等)。
  • 状态变化:页面加载完成后(触发 pageshow 事件),默认显示在当前标签页,此时页面 可见(visibilityState: 'visible')且获得焦点(hasFocus(): true ,直接进入 ACTIVE 状态
  • 状态特点:用户能看到掘金首页,可立即进行交互(如点击文章、输入搜索关键词、滚动页面),JS 脚本正常执行(如渲染列表、监听点击事件)。

这块还可以拆出一道经典大厂面试题 “输入URL并回车之后发生了什么”,下次我将会为大家详细讲解一下。

2. 可能的中间状态转换阶段(用户操作或浏览器优化)

初始加载完毕之后,我们可能会对在掘金页面进行一些操作,或是闲置该页面一段时间,在这个阶段,浏览器页面的状态可能会发生多次转换。

(1)ACTIVE -> PASSIVE

  • 触发行为:用户暂时切换到其他窗口(如打开微信、浏览器标签页在前台,但用户点击了地址栏或浏览器菜单、分屏显示)。
  • 状态转换:页面仍可见(未被完全遮挡),但失去焦点(焦点在其他窗口 / 标签),触发blur事件,从ACTIVE转换为 PASSIVE状态
  • 状态维持:用户未切回当前页面,且页面未被完全遮挡时,保持PASSIVE(核心:可见 + 失焦)。

(2)PASSIVE -> HIDDEN

  • 触发行为:用户进一步操作,将当前标签页切换到后台(如点击浏览器其他标签页、最小化浏览器窗口),导致页面完全不可见
  • 状态转换:页面不可见时,触发visibilitychange事件,document.visibilityState变为hidden,从PASSIVE转换为 HIDDEN状态
  • 状态维持:页面不可见,但脚本仍可运行(如定时器、网络请求等,需主动节流避免资源浪费)。

(3)HIDDEN -> FROZEN

  • 触发行为:页面在HIDDEN状态停留一段时间(如几分钟),且浏览器判断其无实时数据需求(如无持续的 WebSocket 连接、定时器不频繁)。
  • 状态转换:浏览器为节省 CPU 资源,主动冻结页面,触发freeze事件,从HIDDEN转换为 FROZEN状态
  • 状态维持:页面不可见,且脚本执行暂停(定时器、事件监听等暂时失效),但 DOM 和 JS 状态保留(如变量、DOM 树)。

(4)FROZEN -> HIDDEN

  • 触发行为:用户切回被冻结的标签页(页面仍未完全可见,如刚切换回来但焦点在其他地方)。
  • 状态转换:浏览器恢复脚本执行,触发resume事件,从FROZEN转换回 HIDDEN状态
    • 补充:用户切回冻结标签页时,首先触发resume事件恢复脚本执行,此时:
      • 若页面变为可见但失焦(如切回后焦点在其他标签),直接进入PASSIVE
      • 若页面仍不可见(如切回后立即又切换到其他标签),才进入HIDDEN
      • 需明确 “页面仍未完全可见” 是指切回后未显示在屏幕上,而非仅失焦。

(5)HIDDEN/PASSIVEACTIVE

  • 触发行为:用户切回掘金标签页,且页面获得焦点(如点击页面内容)。
  • 状态转换
    • 若从HIDDEN切回:先触发visibilitychange事件(visibilityState变为visible),页面可见但未获焦时先到PASSIVE;获焦后触发focus事件,转换为ACTIVE
    • 若从PASSIVE切回:获焦时触发focus事件,直接转换为ACTIVE

(6)极端情况:HIDDEN/FROZENDISCARDED

  • 触发行为:浏览器内存压力过大(如打开过多标签页),且当前页面在HIDDENFROZEN状态(优先级低)。
  • 状态转换:浏览器静默销毁页面状态(DOM、JS 上下文、变量全清空),进入 DISCARDED状态(无触发事件,被动销毁)。
  • 后续处理:用户再次切回该标签页时,浏览器强制重新加载页面,触发pageshow事件(document.wasDiscarded = true),需重新构建页面状态,最终根据焦点进入ACTIVEPASSIVE

3. 页面入土(关闭):HIDDENTERMINATED

当用户主动关闭掘金标签页或导航离开(如输入新网址、点击其他链接)时,页面就会彻底销毁,入土为安。流程如下:

  • 第一步:页面先从当前状态(可能是ACTIVE/PASSIVE/HIDDEN)转换为HIDDEN(页面不可见)。
  • 第二步:触发beforeunload事件(可通过event.preventDefault()弹窗询问用户是否离开)。
  • 第三步:若用户确认关闭,触发pagehide事件(event.persisted = false,表示页面不被缓存)。
  • 第四步:最终触发unload事件,页面完全卸载,进入 TERMINATED 状态(资源释放,无法恢复)。
    • 在现代浏览器中,为支持 “往返缓存(bfcache)”,有可能会跳过unload事件(若页面被缓存,pagehide事件的persistedtrue,且不会触发unload)。

三. 常见的状态混淆

HIDDENFROZEN 之分

HIDDEN(隐藏)和FROZEN(冻结)是页面生命周期中两个容易混淆的状态,因为它们都是页面不可见,但是两者之间亦有差距。

触发条件不同

  • 进入HIDDEN的条件
    由用户操作直接导致页面不可见,例如:
    • PASSIVE状态(可见但失焦)切换到其他标签页,触发visibilitychange事件,document.visibilityState变为hidden
    • FROZEN状态切回时(用户切回被冻结的标签页),先触发resume事件恢复脚本执行,暂时进入HIDDEN(若页面仍未完全可见)。
  • 进入FROZEN的条件
    由浏览器主动触发,基于HIDDEN状态的 “优化判断”,例如:
    • 页面处于HIDDEN状态一段时间后,浏览器检测到该页面无实时数据需求(如无 WebSocket 连接、无高频定时器),为节省 CPU 资源,触发freeze事件,强制暂停脚本,进入FROZEN

脚本运行状态与资源消耗不同

  • HIDDEN状态
    • 脚本仍在运行:定时器(setTimeout/setInterval)、网络请求(fetch/XMLHttpRequest)、事件监听(如scroll/resize)等会正常执行;
    • 资源消耗较高:若不主动节流(如停止不必要的定时器、请求),会持续占用 CPU / 网络资源,可能影响浏览器整体性能
  • FROZEN状态
    • 脚本被暂停:JS 执行完全中断,定时器不会触发,未完成的网络请求可能被挂起,事件监听暂时失效;
    • 资源消耗极低:浏览器会冻结页面的 JS 上下文,仅保留 DOM 和基础状态,大幅降低 CPU 占用,是浏览器 “节能” 的核心手段

状态转换关系

  • HIDDENFROZEN的 “前置状态”:
    页面必须先进入HIDDEN(不可见),才可能被浏览器判断为 “可冻结”,进而进入FROZEN;反之,FROZEN状态只能从HIDDEN转换而来。

  • 离开路径不同:

    • HIDDEN可转换为多种状态:用户切回可见但未获焦→PASSIVE;浏览器冻结→FROZEN;用户关闭标签→TERMINATED;内存不足→DISCARDED
    • FROZEN的离开路径有限:用户切回被冻结的标签页→触发resume事件,恢复脚本执行,回到HIDDEN(若仍不可见)或PASSIVE(若可见);内存仍不足→DISCARDED

DISCARDEDTERMINATED 之分

DISCARDED(丢弃)和TERMINATED(终止)是页面生命周期中两种 “最终态”(状态无法自发恢复),它们都会将DOM、JS 上下文、变量等原状态全清空,所以我们常常会将二者弄混,但其实它们的差别还是挺大的。

触发原因不同

  • DISCARDED浏览器的被动行为用户通常对其无感知,仅发生在页面处于HIDDEN(不可见但脚本运行)或FROZEN(不可见且脚本暂停)状态时,因浏览器内存严重不足(如打开过多标签页、内存泄漏),浏览器为保证其他页面正常运行,静默丢弃该页面的状态(无事件通知开发者)。
  • TERMINATED用户的主动行为用户对其明确感知,由用户明确操作导致,如:关闭标签页、点击链接导航到新页面、在地址栏输入新 URL 并回车等。是用户预期内的 “页面关闭” 行为。

事件触发差异

  • DISCARDED无任何事件通知,浏览器在内存不足时会 “静默丢弃” 页面,不会触发beforeunloadpagehideunload等事件(开发者无法感知,也无法在丢弃前执行清理逻辑)。
  • TERMINATED有完整事件序列,卸载前会触发一系列事件,让开发者有机会处理收尾工作:
    • beforeunload(可取消,用于提示用户 “是否离开”);
    • pagehideevent.persisted = false,表示页面不被缓存);
    • 最终触发unload(资源释放),但是在现代浏览器中,为支持 “往返缓存(bfcache)”,有可能会跳过unload事件(若页面被缓存,pagehide事件的persistedtrue,且不会触发unload);unload仅在页面完全不被缓存persisted: false)时可能触发,但时机不可靠(如浏览器崩溃、进程被杀时不会触发)
      • 也就是说,unload事件在现代浏览器中可靠性较低,不建议依赖它处理关键清理逻辑,推荐优先使用pagehide

实用性不同:状态恢复的可能性与方式

  • DISCARDED可间接恢复(需重建),若标签页仍在浏览器标签栏中(用户未关闭),当用户切回该标签页时,浏览器会强制重新加载页面(触发loadpageshow事件),且document.wasDiscarded会被标记为true(告知开发者页面曾被丢弃)。此时页面需重新执行初始化逻辑(重建 DOM、恢复数据等)。
  • TERMINATED完全不可恢复,页面已被彻底卸载,标签页不存在(或已导航至新页面),无法通过 “切回标签页” 恢复。用户必须重新输入 URL、从历史记录打开或点击书签,才能重新加载页面(相当于全新打开)。
  • 示例场景: 如果用户在页面上填写了表单(未提交),正常情况下可能存在内存中的变量里。
    • DISCARDED:如果情况是页面被丢弃,虽然内存数据已被销毁,但是开发者可检测到wasDiscarded = true,此时可尝试从localStorage等持久化存储中读取之前保存的临时数据,提示用户 “恢复上次输入”,避免用户操作丢失,这会极大提升用户的使用体验,毕竟谁也不想自己要重复输入一大堆数据。
    • TERMINATED:如果情况是用户明确要关闭,这时候可能就是用户要保护自己的隐私,不希望恢复之前输入的数据,这时候就不应该去恢复数据,避免了用户可能会发生的社死,这也是会极大提升用户的使用体验。

结语

作为一名前端工作者,了解一定的浏览器原理无疑是至关重要的,这是每一个前端工作者的基本功。希望这篇文章能够帮助到大家的浏览器页面生命周期学习,如果本篇文章有一些错误,请在评论区指出,大家一起进步哦,谢谢支持🙏。