前言
我相信大家都做过这样一件事:打开浏览器输入某个网址(比如https://juejin.cn
),然后回车进入网站浏览一番,最后心满意足地关闭页面。这样我们看来再正常不过的事情,其实包含了一个浏览器页面由生到死的全过程。本文又称“从输入URL到浏览器页面入土全过程”,下面我将为你介绍从用户触发导航(如输入URL、点击链接)到页面最终被关闭的全过程。
一. 页面生命周期概述
《黑神话:悟空》这款游戏中,作为天命人,我们要找到大圣的“六根”将其复活,而在浏览器页面中,同样也有“六根”,它们分别是ACTIVE
(活跃)、PASSIVE
(被动可见)、HIDDEN
(隐藏)、TERMINATED
(终止)、FROZEN
(冻结)、DISCARDED
(丢弃),是为浏览器页面的六个核心生命周期状态。
生命周期状态视图
- 从谷歌开发者文档网站,我找到了这样一张图来说明浏览器页面生命周期的各个状态。
原视图
翻译解析视图
- 我没找到中文版的视图,所以自行翻译解析了一下,如有错误,请帮我指正。
- 注意:
- 可靠事件: 浅黄色事件框代表的“可靠事件”,指的是浏览器稳定支持,触发时机可信赖,如
visibilitychange
。 - 不可靠事件: 橙色事件框代表的“不可靠事件”,指的是触发时机不确定,或旧浏览器兼容性差,如早期
pagehide
行为
- 可靠事件: 浅黄色事件框代表的“可靠事件”,指的是浏览器稳定支持,触发时机可信赖,如
六大核心状态
- 核心状态分类: 从图中可以看出,这六个核心状态分为用户交互驱动(蓝色,即为用户交互有关的状态)以及浏览器优化驱动(紫色,即为浏览器自发的优化行为有关的状态)两大类。
- 用户交互驱动:
ACTIVE
(活跃):页面可见且获得焦点,用户正在交互(用户正在当前标签页交互,如点击、输入、滚动)。PASSIVE
(被动可见):页面可见但失去焦点(如多窗口并列,焦点在其他窗口)HIDDEN
(隐藏):页面不可见(如切换到其他标签、最小化窗口),脚本仍可运行(需主动节流)TERMINATED
(终止):页面完全卸载(资源释放,无法恢复,如关闭标签页、导航到新页面)。
- 浏览器优化驱动:
FROZEN
(冻结):页面不可见且脚本暂停(通常为页面处于HIDDEN
一段时间之后,浏览器主动冻结,保留 DOM/JS 状态,节省 CPU)。DISCARDED
(丢弃):页面状态被彻底清空(DOM、JS 上下文、变量全销毁,浏览器因内存压力丢弃)。
- 用户交互驱动:
- 核心状态总结:
状态 | 定义 | 进入条件(前状态 → 触发事件 / 行为) | 离开条件(后状态 → 触发事件 / 行为) |
---|---|---|---|
ACTIVE | 页面同时可见且获得焦点 | 1️⃣ PASSIVE → 用户点击标签页(获焦),触发 focus 事件;2️⃣ pageshow → 页面加载后,document.hasFocus() === true | PASSIVE → 用户切换到其他窗口 / 标签(失焦),触发 blur 事件 |
PASSIVE | 页面可见但失去焦点 | 1️⃣ ACTIVE → 用户切换到其他窗口(失焦),触发 blur 事件;2️⃣ pageshow → 页面加载后,document.hasFocus() === false | 1️⃣ 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 (可取消)→ pagehide (event.persisted = false ,不缓存)→ unload ;4️⃣ DISCARDED → 浏览器内存严重不足,直接销毁页面状态 |
FROZEN | 页面不可见且脚本暂停 | HIDDEN → HIDDEN 状态下,浏览器判断 “可冻结”(如无实时数据需求),触发 freeze 事件 | 1️⃣ HIDDEN → 用户切回冻结标签,触发 resume 事件;2️⃣ DISCARDED → 冻结后内存仍不足,浏览器强制清空状态 |
DISCARDED | 页面状态被彻底清空 | 1️⃣ HIDDEN → 浏览器内存严重不足,静默丢弃;2️⃣ FROZEN → 冻结后内存仍不足,静默丢弃 | ACTIVE/PASSIVE (重新加载后)→ 用户切回丢弃标签,浏览器强制重新加载,触发 load /pageshow (document.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/PASSIVE
→ ACTIVE
- 触发行为:用户切回掘金标签页,且页面获得焦点(如点击页面内容)。
- 状态转换:
- 若从
HIDDEN
切回:先触发visibilitychange
事件(visibilityState
变为visible
),页面可见但未获焦时先到PASSIVE
;获焦后触发focus
事件,转换为ACTIVE
。 - 若从
PASSIVE
切回:获焦时触发focus
事件,直接转换为ACTIVE
。
- 若从
(6)极端情况:HIDDEN/FROZEN
→ DISCARDED
- 触发行为:浏览器内存压力过大(如打开过多标签页),且当前页面在
HIDDEN
或FROZEN
状态(优先级低)。 - 状态转换:浏览器静默销毁页面状态(DOM、JS 上下文、变量全清空),进入
DISCARDED
状态(无触发事件,被动销毁)。 - 后续处理:用户再次切回该标签页时,浏览器强制重新加载页面,触发
pageshow
事件(document.wasDiscarded = true
),需重新构建页面状态,最终根据焦点进入ACTIVE
或PASSIVE
。
3. 页面入土(关闭):HIDDEN
→ TERMINATED
当用户主动关闭掘金标签页或导航离开(如输入新网址、点击其他链接)时,页面就会彻底销毁,入土为安。流程如下:
- 第一步:页面先从当前状态(可能是
ACTIVE
/PASSIVE
/HIDDEN
)转换为HIDDEN
(页面不可见)。 - 第二步:触发
beforeunload
事件(可通过event.preventDefault()
弹窗询问用户是否离开)。 - 第三步:若用户确认关闭,触发
pagehide
事件(event.persisted = false
,表示页面不被缓存)。 - 第四步:最终触发
unload
事件,页面完全卸载,进入TERMINATED
状态(资源释放,无法恢复)。- 在现代浏览器中,为支持 “往返缓存(bfcache)”,有可能会跳过
unload
事件(若页面被缓存,pagehide
事件的persisted
为true
,且不会触发unload
)。
- 在现代浏览器中,为支持 “往返缓存(bfcache)”,有可能会跳过
三. 常见的状态混淆
HIDDEN
与 FROZEN
之分
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 占用,是浏览器 “节能” 的核心手段。
状态转换关系
-
HIDDEN
是FROZEN
的 “前置状态”:
页面必须先进入HIDDEN
(不可见),才可能被浏览器判断为 “可冻结”,进而进入FROZEN
;反之,FROZEN
状态只能从HIDDEN
转换而来。 -
离开路径不同:
HIDDEN
可转换为多种状态:用户切回可见但未获焦→PASSIVE
;浏览器冻结→FROZEN
;用户关闭标签→TERMINATED
;内存不足→DISCARDED
。FROZEN
的离开路径有限:用户切回被冻结的标签页→触发resume
事件,恢复脚本执行,回到HIDDEN
(若仍不可见)或PASSIVE
(若可见);内存仍不足→DISCARDED
。
DISCARDED
与 TERMINATED
之分
DISCARDED
(丢弃)和TERMINATED
(终止)是页面生命周期中两种 “最终态”(状态无法自发恢复),它们都会将DOM、JS 上下文、变量等原状态全清空,所以我们常常会将二者弄混,但其实它们的差别还是挺大的。
触发原因不同
DISCARDED
:浏览器的被动行为,用户通常对其无感知,仅发生在页面处于HIDDEN
(不可见但脚本运行)或FROZEN
(不可见且脚本暂停)状态时,因浏览器内存严重不足(如打开过多标签页、内存泄漏),浏览器为保证其他页面正常运行,静默丢弃该页面的状态(无事件通知开发者)。TERMINATED
: 用户的主动行为,用户对其明确感知,由用户明确操作导致,如:关闭标签页、点击链接导航到新页面、在地址栏输入新 URL 并回车等。是用户预期内的 “页面关闭” 行为。
事件触发差异
DISCARDED
:无任何事件通知,浏览器在内存不足时会 “静默丢弃” 页面,不会触发beforeunload
、pagehide
、unload
等事件(开发者无法感知,也无法在丢弃前执行清理逻辑)。TERMINATED
:有完整事件序列,卸载前会触发一系列事件,让开发者有机会处理收尾工作:beforeunload
(可取消,用于提示用户 “是否离开”);pagehide
(event.persisted = false
,表示页面不被缓存);- 最终触发
unload
(资源释放),但是在现代浏览器中,为支持 “往返缓存(bfcache)”,有可能会跳过unload
事件(若页面被缓存,pagehide
事件的persisted
为true
,且不会触发unload
);unload
仅在页面完全不被缓存(persisted: false
)时可能触发,但时机不可靠(如浏览器崩溃、进程被杀时不会触发)。- 也就是说,
unload
事件在现代浏览器中可靠性较低,不建议依赖它处理关键清理逻辑,推荐优先使用pagehide
。
- 也就是说,
实用性不同:状态恢复的可能性与方式
DISCARDED
:可间接恢复(需重建),若标签页仍在浏览器标签栏中(用户未关闭),当用户切回该标签页时,浏览器会强制重新加载页面(触发load
或pageshow
事件),且document.wasDiscarded
会被标记为true
(告知开发者页面曾被丢弃)。此时页面需重新执行初始化逻辑(重建 DOM、恢复数据等)。TERMINATED
:完全不可恢复,页面已被彻底卸载,标签页不存在(或已导航至新页面),无法通过 “切回标签页” 恢复。用户必须重新输入 URL、从历史记录打开或点击书签,才能重新加载页面(相当于全新打开)。- 示例场景: 如果用户在页面上填写了表单(未提交),正常情况下可能存在内存中的变量里。
DISCARDED
:如果情况是页面被丢弃,虽然内存数据已被销毁,但是开发者可检测到wasDiscarded = true
,此时可尝试从localStorage
等持久化存储中读取之前保存的临时数据,提示用户 “恢复上次输入”,避免用户操作丢失,这会极大提升用户的使用体验,毕竟谁也不想自己要重复输入一大堆数据。TERMINATED
:如果情况是用户明确要关闭,这时候可能就是用户要保护自己的隐私,不希望恢复之前输入的数据,这时候就不应该去恢复数据,避免了用户可能会发生的社死,这也是会极大提升用户的使用体验。
结语
作为一名前端工作者,了解一定的浏览器原理无疑是至关重要的,这是每一个前端工作者的基本功。希望这篇文章能够帮助到大家的浏览器页面生命周期学习,如果本篇文章有一些错误,请在评论区指出,大家一起进步哦,谢谢支持🙏。