用 Node 直连 Chrome 的 DevTools Protocol(WebSocket 直连,不走 Puppeteer / Playwright),对一个已经渲染完的页面做鼠标拖拽:按顺序发 Input.dispatchMouseEvent 的 mouseMoved → mousePressed → 一连串 mouseMoved(30~45 步) → mouseReleased。

多数情况下整串拖拽 200~400ms 就走完,但偶发一次会卡到 60~80 秒。卡的方式很特别:CDP 命令本身都有正常回包(没有 timeout),只是相邻两个事件之间被拉长了约 1~3 秒,累积成一分多钟。同一台机器、同一个页面、同一段代码,紧接着重试第二次就恢复正常。

想确认这段延迟究竟发生在哪一层。我目前的理解是:

Input.dispatchMouseEvent 属于 Input domain,事件要经 browser 进程转给 renderer 的合成输入通道。如果 renderer 主线程正忙(长任务、样式重算、大量布局),输入事件会在队列里等派发,而 CDP 的响应可能只表示「事件已入队」,不代表已经被页面消费 —— 也就是说命令 ack 与事件实际派发是两件事。

不清楚的是:这个等待队列的临界点在哪,以及在发送前有没有可观测的信号能提前判断 renderer 是否空闲。

// 拖拽的骨架,steps 约 34~46 const mouse = (type, x, y) => cdp.send('Input.dispatchMouseEvent', { type, x: Math.round(x), y: Math.round(y), button: 'left', clickCount: 1 }); for (let i = 1; i <= steps; i += 1) { const t = i / steps; const eased = 1 - (1 - t) ** 2.6; await mouse('mouseMoved', box.x + distance * eased, box.y); await sleep(14 + Math.random() * 16); }

做过哪些尝试

  1. 把每个 mouseMoved 之间的 sleep 从 14~30ms 缩短到 8ms、步数从 40 步压到 20 步 —— 偶发延迟一点没变,所以不是「事件条数多」或「间隔太长」导致的。
  2. 给 Input.dispatchMouseEvent 显式传 timestamp(Date.now() / 1000 递增),让 Chrome 按合成时间轴派发 —— 无改善。
  3. 拖拽前先 Runtime.evaluate('1') 探一次,确认 renderer 能立刻响应 —— 探测很快返回,紧接着的拖拽照样卡。
  4. 用 Performance.getMetrics 前后取差值,卡顿期间 TaskDuration 确实在涨,但看不出是页面里哪段脚本占住的。
  5. 排除了网络因素:整个过程页面没有再发请求,也没有在导航。

期望的结果

  • 能确定这段延迟是 renderer 主线程被长任务占住导致的输入排队,还是 CDP 连接 / browser 进程调度层的问题;
  • 如果是前者,想在发拖拽之前做一次「主线程是否空闲」的前置判断。想到的方向有 PerformanceObserver 的 longtask、requestIdleCallback 的超时回调、以及 scheduler.postTask 的优先级队列,但不确定哪一个的信号在跨进程输入派发这件事上真的有效;
  • 换个角度也可以:如果 CDP 这边能拿到「事件已被页面消费」的确认(而不是仅仅入队),那我就能用确认来做节流,而不是靠猜。

实际得到的结果

只能观察到「偶发 + 重试即好」,找不到可稳定复现的触发条件。用 Runtime.evaluate 读 document.readyState、读主线程时间都是正常的,没有任何报错。