应使用 performance.now() 记录首次输入和最后交互时间,结合 visibilitychange 和 submit 事件多点采集,通过 fetch 独立上报或 post 携带隐藏字段传时长,避免依赖 focus/blur 和 beforeunload。

怎么用 JavaScript 监听表单填写开始和结束时间
直接看关键点:不能只靠 focus 和 blur,因为用户可能切到其他标签页、按 Alt+Tab、甚至没触发 blur 就关了页面。真实填写时长得靠「首次输入」+「最后交互」组合判断。
实操建议:
- 监听
input、change、keydown(防粘滞键),第一次触发时记录startTime - 对整个表单绑定
blur、submit、beforeunload,任一触发都更新lastActiveTime - 别只监听单个
<input>,用事件委托到form元素上,避免漏掉动态添加的字段 - 注意移动端:软键盘弹出不一定会触发
focus,但input事件基本可靠
为什么 performance.now() 比 date.now() 更适合测填写耗时
填写时间精度要求高时,Date.now() 会受系统时钟调整影响(比如 NTP 同步后倒拨几毫秒),导致负值或跳变;而 performance.now() 是单调递增的高精度时间戳,单位是毫秒且带小数,浏览器支持也已稳定(IE10+,现代环境无兼容问题)。
常见错误现象:
立即学习“前端免费学习笔记(深入)”;
- 用
Date.now()算出 -23ms 或 42s 的异常值 → 实际是系统时间被校准了 - 在 iframe 里测时没考虑跨域限制 →
performance在同源 iframe 中可用,跨域则抛错,需加try/catch - 没做初始化校验,直接调
performance.now()→ 极少数旧安卓 webview 可能未定义,建议先if (performance && performance.now)
提交时怎么把填写时长安全传给后端
别把时间戳拼在 URL 里 GET 提交,也别硬塞进隐藏字段再由用户提交——容易被篡改。最稳妥的是在 submit 事件中,用 fetch 或 XMLHttpRequest 额外发一次轻量埋点请求,或者把计算好的时长作为额外字段走原表单 POST(前提是后端信任该字段,且不用于关键逻辑校验)。
使用场景与参数差异:
- 如果表单本身是
POST,直接加一个<input type="hidden" name="fill_duration_ms">,js 在 submit 前填值 → 简单但可被禁用 JS 或调试器修改 - 用
fetch('/api/track', { method: 'POST', body: json.stringify({ form_id: 'login', duration: 3287 }) })→ 独立上报,不影响主流程,但需后端有对应接口且处理幂等 - 注意 CORS:如果埋点域名和表单域名不同,要确保后端响应了
access-Control-Allow-Origin
beforeunload 里记录时间为什么经常失效
因为现代浏览器(chrome 80+、firefox 90+)大幅限制了 beforeunload 的执行能力:不允许异步操作、禁止 fetch、XMLHttpRequest 大概率失败,只能同步写入 localStorage 或触发极简 alert(后者已被大多数浏览器屏蔽)。它现在只适合存个标记,不能指望它把数据发出去。
性能与兼容性影响:
- 试图在
beforeunload里发fetch→ 控制台报Failed to execute 'fetch' on 'Window': Cannot construct a Request with a Request whose mode is 'navigate' - 改用
localStorage.setItem('form_fill_end', Date.now())→ 可行,但下次打开页面才能读,无法实时上报 - 真正可靠的兜底方案是结合
visibilitychange事件:当document.visibilityState === 'hidden'时记下最后活跃时间,比beforeunload更早、更稳定
复杂点在于用户行为不可预测——切屏、锁屏、网络中断、JS 异常崩溃都会打断时间链。最务实的做法是:记录多个时间点(首次输入、最后交互、页面隐藏、提交),后端按策略取最合理的一组算出有效填写时长。别指望前端单点数据绝对准确。