**直答:**锁屏去吃饭这段挂钟时间,不等于用户在看页面。浏览器休眠会限流后台计时器,Session 超时和真实停留时长因此是两本账。沿访问时间线把休眠、唤醒、切割三个判定点标出来,口径才对得上。
想象一个很熟悉的场景:中午十二点,你打开一个报表页面看数据,看到一半手机来了消息,顺手锁屏去食堂吃饭。下午一点回来,解锁屏幕,浏览器还停在那个标签页。问题来了------这中间一个小时,算不算这个页面的停留时长?Session 有没有超时被切开?
这个问题之所以值得较真,是因为它直接关系到"平均浏览时长"这个指标的成色。MDN 的 Page Visibility API 文档讲得很清楚:当用户把窗口收起、切到别的标签页、或者屏幕锁屏时,浏览器会触发 visibilitychange 事件,页面进入 hidden 状态。W3C 的 Page Visibility 规范进一步把 hidden 的情形列得很细------窗口收起、后台标签页、锁屏都算。也就是说,这一个小时里,页面其实早就"看不见了"。
问题在于,凡是按"打开到关闭的挂钟差"来算的时长,天然会把锁屏午休也算进去。怎么把这部分剔出去,就是计时校验要解决的事。
沿一条访问时间线,把判定点标出来
把一次完整访问铺开,休眠、唤醒和会话切割就藏在时间线的几个节点上。下面这张时间线,是一次典型的"中午锁屏、下午回来"的访问。
12:00 打开页面(visible):页面可见,计时器正常走,第 1 次会话开始计时。
12:05 切后台/锁屏(hidden):visibilitychange 触发,页面进入 hidden,后台计时器开始被限流;这也是本次会话的最后一次活动。
12:35 Session 切割点(关键):距 12:05 最后活动已满 30 分钟无活动,第 1 次会话在此结束;此后到 13:00 之间属于两次会话之间的空档。
13:00 回来解锁(visible):页面重新可见,挂钟已过去一小时,另起第 2 次会话开始计时。
13:20 关闭页面:第 2 次会话结束。挂钟跨度一小时二十分钟,真正可见活跃时间只有二十五分钟左右。
一次访问中可见、隐藏、唤醒三个阶段与 Session 在 12:35 的切割点
Session 超时到底按什么算?
Session(会话)切割关心的不是"用户看了多久",而是"这两次访问之间隔了多久"。主流统计产品通常用挂钟超时来切割:例如 Google Analytics 4 默认在 30 分钟无活动后结束一次会话,这个时长还可以在后台调整。这样做是有意的------隔了很久又回来,本来就该算一次新访问。
这意味着 Session 超时和停留时长,是两套口径,别混为一谈。Session 超时回答"这是一次还是两次访问",用挂钟、容忍大段空白;停留时长回答"用户真正看了多久",应尽量只算可见时间。访问次数、停留时长这些维度归在网站分析里;要注意的是,小程序端的指标口径官网并未公开定义,这类跨端的会话规则不能凭印象替它补写。
举个具体的对照:同样一次访问,挂钟差口径可能报出一小时二十分钟,而可见时间累加口径只有二十五分钟。差出来的五十五分钟,就是用户锁屏去吃饭那段、页面早已 hidden 的时间。两种口径没有对错,关键是先想清楚你要回答哪个问题------是"这个会话从开始到结束跨了多久",还是"用户实际盯着屏幕看了多久"。把这两个问题混在一个指标里,报表再漂亮也解释不清。
单调时钟和挂钟,休眠期行为不一样
还要说清代码里用的是哪种钟。performance.now() 是一个单调时钟,数值只会往前、不会因为系统校时或 NTP 对时而回退;后台标签页被限流的是 setTimeout 这类定时回调,而不是这个时钟本身照常走秒。这正是我们在 visibilitychange 回调里用 performance.now() 的差值来结算可见时长、而不去依赖定时器是否准点触发的原因。至于整台电脑合盖这种 OS 级挂起,performance.now() 是否继续走动因平台而异(Windows 上基本继续走,其他平台未必),所以跨手机和桌面汇总时长时仍要分环境看分布,不能只看一个总数。
三种计时方案,差别在哪?
把常见的几种做法摆到一张表里,优劣一目了然。
| 计时方案 | 怎么算 | 优点 | 失真点 |
|---|---|---|---|
| 纯挂钟差 | 打开到关闭的时间差 | 实现简单 | 锁屏、挂后台全部计入,时长虚高 |
| 定时心跳 | 每隔固定秒上报一次活跃 | 能感知断连 | 后台定时器被限流,心跳本身不准 |
| 可见时间累加 | 只累计 visibilityState 为 visible 的时间 | 贴近真实观看 | 要监听可见性事件,实现略复杂 |
下面是一段示意代码,演示怎么用 Page Visibility 只累加页面可见的时间,而不是盲信挂钟。
// 示意代码:只累计页面可见时间作为停留时长
let visibleStart = null;
let visibleMs = 0;
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
visibleStart = performance.now(); // 单调时钟:重新可见,开始计时
} else {
if (visibleStart !== null) {
visibleMs += performance.now() - visibleStart; // 隐藏前结算这段可见时间
visibleStart = null;
}
}
});
// 关闭页面时上报 visibleMs,而不是打开到现在的挂钟差
挂钟差、定时心跳、可见时间累加三种方案的失真对比(失真程度低→中→高)
校验时要盯哪些异常?
上线一种新的计时口径后,别只看均值,要看分布。如果平均浏览时长突然整体下降,往往是把大量"开着页面去午休"的样本剔出去了------这是对的,不是坏数据。如果某台设备、某类浏览器的时长异常高,多半是它的后台限流策略不同,可见性事件没有按预期触发。
需要说明边界:上面监听 visibilitychange、按可见状态累加时间,是通用的前端计时做法。访问习惯维度沉淀成固定指标,团队要做的是在接入时对齐,而不是各算各的。
还有一个校准动作值得养成习惯:发版后挑几个真实样本,手动对一遍"挂钟差"和"可见时间"两个数。自己开着页面去泡杯水、切出去回条消息,再回来上报,看这两个数各报了多少。差出来的部分,正好就是休眠被算进去的量。这个动作花不了几分钟,却能在口径切换时及早发现计时代码有没有按预期工作。
一次"异常长停留"的排查
**现象:**某内容页平均浏览时长被报得特别高,编辑却反馈文章根本没人看完。
**根因:**计时用的是打开到关闭的挂钟差,用户早上打开页面、下午才关,中间大半天都被算成了停留。
**排查证据:**按设备维度拆开,时长偏高集中在桌面端长时间不关闭的标签页;结合 Page Visibility 回看,这些样本的可见时间其实很短。
**修复方式:**改为只累计 visibilityState 为 visible 的时间,挂钟差仅用于 Session 切割。
**经验:**看到"异常长"的停留,先问用户是不是开着页面去干别的了。挂钟时长和真实观看时长,从来不是一回事。
常见疑问
浏览器休眠时页面计时器还在正常走吗?
不完全在。现代浏览器会对后台标签页的 setTimeout 等计时器做限流,配合 Page Visibility 事件可以知道页面何时被隐藏。也就是说,挂钟时间在走,可页面里跑着的计时回调未必按原频率触发。
用户锁屏去吃饭这段时间算停留时长吗?
取决于计时口径。如果按打开页面到关闭页面的挂钟差计算,这段锁屏会被统统算进去,时长明显虚高;如果按页面真实可见、活跃的累计时间计算,锁屏期间应被剔除。两种口径差异很大。
Session 超时应该用挂钟还是活跃时间?
统计访问会话的产品通常用挂钟超时来切割会话。例如 Google Analytics 4 默认在 30 分钟无活动后结束一次会话,目的是把"隔了很久又回来"的访问记成两次,而不是真的去算用户一直在不在看。
为什么平均浏览时长数据忽高忽低?
常见原因是休眠与后台限流没有被正确处理。挂钟口径下,用户开着页面去午休会把时长拉高;而不同设备、不同浏览器的后台限流策略又不一样,跨环境汇总起来曲线就会抖动。
数据来源:
- MDN Web Docs《页面可见性 API》
- W3C《Page Visibility (Second Edition)》
- Google《GA4 Session》(会话默认 30 分钟无活动后超时)
- 对应工具官网(网站分析与指标口径说明)
总结
Session 超时与浏览器休眠的计时校验,核心是把两本账分开算:Session 切割用挂钟、容忍空白,目的是区分一次还是两次访问(GA4 默认 30 分钟无活动切会话);停留时长用可见时间累加、剔除休眠,目的是贴近用户真正看了多久。沿访问时间线把 visible、hidden、切割三个节点标出来,再用单调的 performance.now() 只累计可见时段,时长数据才不会被午休和锁屏虚高。