TimeMe.js闲置检测算法揭秘:250ms轮询与事件驱动的平衡之道
【免费下载链接】TimeMe.jsA JavaScript library to accurately time how long a user views a web page, disregarding idle time and time when the tab or window is minimized.项目地址: https://gitcode.com/gh_mirrors/ti/TimeMe.js
TimeMe.js 是一个 JavaScript 页面浏览时长统计库,能准确记录用户"真正在看"网页的时间,自动剔除闲置和切走标签页的时段。它的闲置检测既不是死板的定时轮询,也不是纯靠事件监听,而是用250ms 轮询 + 事件驱动的混合策略,在精度与性能之间找到了巧妙的平衡点。下面带你拆解这套算法的设计思路。
一、为什么"简单计时"不可靠?
如果你用Date.now()在页面加载和离开时各记一笔,算出来的时长包含大量水分:
- 用户切到别的标签页去刷短视频了 ⏳
- 用户泡杯咖啡回来,鼠标半天没动 🍵
- 窗口被最小化挂着过夜
TimeMe.js 要统计的是真实交互时长:只有用户在页面、且正在操作(或刚操作完)的时间才算数。为此,它必须回答两个问题:
- 用户什么时候"人没了"(切标签页、失焦)?
- 用户什么时候"闲下来了"(长时间无鼠标键盘输入)?
答案正是两种监听手段的分工协作。
二、心跳线:每 250ms 一跳的闲置计数器
算法的核心是一个叫checkIdleStateRateMs的常量,被固定设为250(毫秒),定义在 timeme.js 第 41 行:
- 库通过
setInterval每 250ms 触发一次checkIdleState()(第 344~348 行) - 每跳一次,闲置时长累加器
currentIdleTimeMs就+250ms - 一旦
currentIdleTimeMs > idleTimeoutMs(默认 30 秒),立刻判定用户闲置,停止所有计时器
也就是说,闲置的"进度条"只靠轮询往前推。同时这个心跳还顺带完成了另一件事:遍历检查callAfterTimeElapsedInSeconds()注册的回调——用户活跃满多少秒后弹窗提示之类的场景,就是靠它逐跳探测触发的(第 276~289 行)。
| 参数 | 默认值 | 含义 |
|---|---|---|
checkIdleStateRateMs | 250ms | 闲置轮询的"心跳"间隔 |
idleTimeoutMs | 30000ms(30 秒) | 多久无操作判定为闲置 |
三、打断线:4 个事件瞬间"清零"闲置计时
轮询只能保证"闲了多久"算得准,却不能保证"人回来了"算得快。好在用户一有动作,事件会立刻打断闲置倒计时:
listForIdleEvents()(timeme.js 第 338~342 行)给页面挂了 4 个监听器:
| 事件 | 覆盖的场景 |
|---|---|
mousemove | 桌面端移动鼠标 |
keyup | 键盘输入 |
touchstart | 移动端触摸屏幕 |
scroll | 滚动页面 |
任意一个事件触发,都会调用userActivityDetected()(第 209~218 行),做两件事:
- 把
currentIdleCountdown清零、isUserCurrentlyIdle置为false——闲置倒计时瞬间归零,而不是等到下一个 250ms 心跳 - 若此前被判定为闲置,还会触发
callWhenUserReturns()回调,让业务方知道"用户回来了" 💡
一句话总结分工:轮询负责"推进"闲置时间,事件负责"重置"闲置时间。事件保证响应零延迟,轮询保证即使浏览器对某些动作不派发事件,闲置计时也绝不会漏算。
四、标签页切换的捕捉:页面可见性 + 窗口焦点双保险
"用户切走了"是比闲置更严重的流失,TimeMe.js 用两条事件线同时兜住(第 306~336 行):
visibilitychange事件:标签页在浏览器中显示/隐藏时触发。代码还做了向后兼容,依次探测mozHidden、msHidden、webkitHidden等旧版厂商前缀,老浏览器也能用window的blur/focus事件:整个浏览器窗口失去/获得焦点时触发,覆盖"切到另一个 App"的场景
页面隐藏时调用triggerUserHasLeftPageOrGoneIdle():标记用户离开、触发callWhenUserLeaves()回调、并调用stopAllTimers()关闭全部计时段;用户回来时则由triggerUserHasReturned()重新开启计时。
时长本身以"起止时间区间"的方式存储在startStopTimes中(第 82~125 行),一段活跃期就是一条{startTime, stopTime}记录,最终统计时把所有区间的差值累加——所以闲置和离屏的时段天然就不被计入。
五、为什么偏偏是 250ms?
这是整个算法最见功力的取舍:
- 精度下限:最坏情况下,闲置判定会晚最多 250ms,统计误差约 0.25 秒,对"页面浏览时长分析"这种分钟级口径的业务完全无感
- 性能上限:每秒只跑 4 次极轻量的函数,远低于
requestAnimationFrame(每秒约 60 次)的开销,移动端也不心疼电量 - 可被事件纠偏:心跳只是"兜底节拍器",任何真实交互都会被事件立即打断,因此轮询再"笨"也不会造成体感延迟
换成 1 秒轮询省一点 CPU,但闲置判定误差变成秒级;换成 50ms 轮询精度更高,但白白消耗 5 倍资源。250ms 就是精度与成本的最佳公约数⚖️
六、动手验证:Demo 与单元测试
- 打开 demo/index.html:页面顶部有一个实时计时器,闲置阈值被刻意设为 5 秒方便观察——停手 5 秒后计时立刻冻结,动一下鼠标又恢复,切走标签页同样生效
- tests/tests.js 中基于 QUnit 编写了完整的单元测试,覆盖了
callWhenUserLeaves/callWhenUserReturns回调、stopAllTimers等关键路径,浏览器中打开 tests/testrunner.html 即可运行
集成只需几行代码,核心配置就是闲置阈值:
TimeMe.initialize({ currentPageName: "my-home-page", idleTimeoutInSeconds: 30 // 闲置多少秒后停止计时 });详细 API 说明见 README.md,压缩产物为 timeme.min.js,通过npm install timeme.js --save也可安装。
小结
TimeMe.js 的闲置检测算法给了前端一个很好的范式参考:用低频轮询(250ms 心跳)做状态推进与兜底,用高频事件(鼠标/键盘/触摸/滚动/可见性)做即时纠偏,两者各司其职,最终用极小的运行时开销换来了"真实交互时长"的精准度量。如果你也在做页面停留时长、用户参与度分析,这套"粗粒度心跳 + 细粒度中断"的混合思路,值得直接抄作业 ✅
【免费下载链接】TimeMe.jsA JavaScript library to accurately time how long a user views a web page, disregarding idle time and time when the tab or window is minimized.项目地址: https://gitcode.com/gh_mirrors/ti/TimeMe.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考