无头环境下 setInterval 被节流:停留计时任务永远拿不到分的排查

举报
yd_228692151 发表于 2026/11/07 13:58:19 2026/11/07
【摘要】 无头环境下 setInterval 被节流:停留计时任务永远拿不到分的排查页面可见性判定与 CDP 焦点模拟的冲突,会同时造成两个假象:计时器在跑但计数不动,以及"页面明明渲染正常却说自己是 hidden"。 症状一个按"停留时长"给奖励的任务:打开详情页,要求保持浏览 30 秒。自动化脚本把页面打开后,每 5 秒滚动一次模拟阅读,持续 42 秒,服务端返回的进度字段始终是 0:{"tas...

无头环境下 setInterval 被节流:停留计时任务永远拿不到分的排查

页面可见性判定与 CDP 焦点模拟的冲突,会同时造成两个假象:计时器在跑但计数不动,以及"页面明明渲染正常却说自己是 hidden"。

症状

一个按"停留时长"给奖励的任务:打开详情页,要求保持浏览 30 秒。自动化脚本把页面打开后,每 5 秒滚动一次模拟阅读,持续 42 秒,服务端返回的进度字段始终是 0:

{"task_key":"case-view-daily","evaluator":"dwell","need_seconds":30,
 "max_gap_ms":15000,"today_status":"todo","elapsed_seconds":0}

同时页面截图完全正常——导航、正文、侧栏都渲染出来,说明不是白屏也不是加载失败。

第一个判据:影子 DOM 之外,先看 visibilityState

Runtime.evaluate: JSON.stringify({vis:document.visibilityState, hidden:document.hidden})
-> {"vis":"hidden","hidden":true}

document.hasFocus() 却返回 true。这两个字段的含义不同:hasFocus() 只说当前文档在所属窗口内是活动文档,visibilityState 才反映窗口有没有真正呈现给用户。

第二个判据:计时器是否被节流

页面已渲染、hidden=true,接下来要确认 JS 定时器有没有被降频。在页面上下文里跑一个 100ms 的 setInterval,做 1 秒,数实际拍数:

new Promise(res => {
  let n = 0;
  const iv = setInterval(() => { n++; }, 100);
  setTimeout(() => { clearInterval(iv); res(n); }, 1000);
})

正常应该接近 10。实测返回 2。10 倍节流——Chromium 对隐藏页面会把 setInterval 压到最低每秒 1 次(后台再久会进一步降到每分钟 1 次)。

这条把"服务端不认"和"前端没计时"彻底分开了:不是接口拒收,是计时根本走不动。

为什么窗口"看起来没最小化"却还是隐藏

查 Win32 层窗口状态:

IsWindowVisible(hwnd) -> 1
IsIconic(hwnd)        -> 0
GetWindowRect(hwnd)   -> (0, 0, 1280, 860)

窗口可见、未最小化、坐标也在屏幕内。但 SetForegroundWindow 返回 0,GetForegroundWindow() 指向的是别的窗口,标题只有 11。

结论:会话没有可交互的桌面。远程/服务化的 Windows 会话里,进程能在无头环境下跑 GUI,窗口在 Win32 意义上存在,但合成器不把它当作"正在呈现",Chromium 据此判定 hidden。Browser.setWindowBounds 改成 normal、Page.bringToFront、调整窗口坐标,全部无效——它们都在改 Win32 属性,改不动合成器的呈现判定。

解法:用 CDP 的焦点模拟绕过呈现判定

Emulation.setFocusEmulationEnabled { enabled: true }

这一条是唯一生效的。执行后立刻:

{"vis":"visible","hidden":false}

注意它模拟的是 focus,副作用是 visibilityState 一并变成 visible,渲染管线也照常出画面(截图正常)。

两个配套细节:

  1. 导航后要重新打开。Page.navigate 会重置部分 Emulation 状态,必须在新文档加载完再调一次,否则计时器又回到 hidden。
  2. 轮询里顺带保持。计时任务通常每 5 秒往服务端汇报一次累计时长,在每轮轮询前重发一次 setFocusEmulationEnabled 成本可以忽略,能避免页面因长时间无交互被重新降级。

计时器的真实实现决定能不能救

拿到站点混淆后的前端代码,找计时逻辑:

function M(){
  if (P || !S || !i.value) return;      // 未开始 / 已完成 / 未运行
  if (document.hidden || A) return;     // <- 隐藏就直接放弃本轮汇报
  A = true;
  const E = await Yt(S);                // 按 session_id 汇报
  v.value = E.accumulated_ms;           // 服务端返回累计毫秒
  if (E.completed) { /* 发奖 */ }
  A = false;
}
function $(){ i.value = true; q = window.setInterval(M, 5000); M(); }
document.addEventListener('visibilitychange', function(){ !document.hidden && i.value && M(); });

三个可直接用于判断的设计点:

  • 累计时长由服务端持有,前端只负责按 session_id 定期"打卡"。所以改客户端变量没有意义,必须真的把请求发出去。
  • 汇报间隔是 5 秒,而任务要求 max_gap_ms 是 15000。打卡间隔被节流到 1 秒 1 次还是 5 秒 1 次无所谓,但一旦页面隐藏、定时器被压到每分钟 1 次,间隔立刻超过 15 秒,累计时长会被服务端重置。这才是"滚了 42 秒还是 0"的完整因果链。
  • document.addEventListener('visibilitychange', ...) 里那句 !document.hidden && M() 说明站点自己也意识到隐藏期间不该计时——它是设计意图,不是 bug。

修复后同一页面、同样流程:

+5s  {"el":0,"st":"todo","vis":"visible"}
+31s {"el":0,"st":"done","vis":"visible"}

31 秒时 today_status 变为 done,completed_at 有真实时间戳。

排查顺序小结

  1. 截图正常 + 服务端进度为 0 → 先怀疑前端计时,不要先怀疑接口。
  2. 读 document.visibilityState,不要读 hasFocus()。
  3. 用一个 100ms 定时器数 1 秒内的拍数,确认节流倍数。
  4. 窗口属性(IsWindowVisible / IsIconic / GetWindowRect)全部正常但 SetForegroundWindow 返回 0 → 无交互桌面,改窗口属性无用。
  5. 用 Emulation.setFocusEmulationEnabled(true),并在每次导航后重开。
  6. 找到计时器源码确认汇报间隔与 max_gap_ms 的关系——如果汇报间隔大于服务端容忍间隔,节流就等于归零,不是"慢一点"。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。