返回博客

无头截图永远看到动画末态?四个混淆因素逐个击破

写了一段 0.8s 揭幕 + 1.2s 渐出的一次性进场动画,打开真机一切正常。切换到 Playwright / chrome-devtools-mcp 截图验证,每次看到的都是「满屏内容全部揭开」,像动画根本没播—— 于是开始反复改代码,空转二十轮,越改越乱。

最后发现:动画从没出过问题,是观测本身一直落在动画结束之后

现象

  • 每次 navigate reload → sleep 若干秒 → screenshot,截图都是末态(遮罩全收、内容全露)。
  • evaluate 查组件挂载标志,返回 false,门控 flag 已被写回——越看越像「没播」。
  • 真机反复确认,动画运行完全正常。

根因:四个混淆因素叠加

1. 一次性门控跨 reload 不重置

进场动画常用 sessionStorage 做「只播一次」的门控:首次进入写入 flag,之后 reload 跳过。 问题在于 sessionStorage 在 F5/Cmd+R 之间保留,只有标签页/进程完全退出才清空。 无头工具的 reload 指令走的是正常刷新,门控 flag 依然在,动画直接跳过,截图当然是末态。

2. dev server 服旧转换缓存

改长了动画时长(比如从 800ms 改成 5000ms),用 curl localhost:PORT/src/animation.ts 看到新内容, 但浏览器(含 ignoreCache 硬刷新)可能仍拿到 Vite/构建器服务端转换缓存里的旧模块,跑的还是旧时长。 截图依然落在动画结束后,你以为是代码没生效,其实是没有真正到达运行页面。

3. 工具调用往返延迟

navigate reload + sleep + screenshot 每个指令都有 1–5s 的 MCP / IPC 往返延迟, 三步累计轻松 5–9s。对 0.8s+1.2s 的动画来说,截图稳稳落在结束后,跟 sleep 传多少秒无关。

4. 快 easing 把「中段」也变成末态

用了 easeOut 风格的缓动(如 1-(1-t)^2.1),在 t=0.5 时动画进度已完成约 77%。 对揭幕动画来说,「中段」视觉上已经露出大半屏,截图根本分不清是「进行中」还是「刚结束」。

坑:在确认「新代码已到达运行页面」之前,别凭满图截图断定动画坏了

四个因素任意一个命中,截图都会给出「末态」——和动画没播一模一样。 用满图截图推断「代码出问题」是错误的:可能只是观测时间点不对,而动画本身运行完全正常。

正解:逐个击破

击破门控:在 navigate reload 之前用 evaluate 清除门控 flag:

// 比 initScript 更可靠,跨随后的 reload 持久生效
await page.evaluate(() => sessionStorage.removeItem('intro_shown'));

击破 dev server 缓存:改完动画参数后,重启 dev server(不是硬刷新), 或在 evaluate 里直接读 window.__ANIMATION_MS__ 等运行时常量确认新值已到位。

击破工具延迟:把动画时长临时拉到远超工具延迟的值——比如 COVER_MS=12000(12s)。 此时任何截图都稳落在遮罩期,先确证「幕能渲染、遮罩色正确」,再还原时长单独验收。

击破 easing 陷阱:验证时改用线性 easing,或专门截 t≈0.1~0.2 的早段, 让「中段」视觉上仍保留明显的未完成状态。

正解:先把时长拉到 10s+ 确证渲染,再还原时长验收时机
# 验证期临时配置
COVER_MS=12000   # 远超任何工具延迟
EASE=linear      # 中段依然可见

截图看到满幕遮罩 = 渲染 OK,之前是观测落点问题,不是代码问题。 再用 evaluate 采样 canvas/overlay 像素值做二次确认,比纯截图更可靠。

实测区分手法

| 查询方式 | 能区分什么 | |---------|-----------| | evaluate 查挂载标志 | 组件有没有挂上去 | | evaluate 查门控 flag 值 | 动画是被跳过还是真的播了 | | evaluate 采样 canvas/overlay 像素 | 当前帧是遮罩色还是背景色 | | 纯截图 | 无法区分「没渲染」和「已结束」|

一句话外卖

无头截图验证亚秒动画前,先把时长拉到 10s+ 排除观测落点问题;否则你调的是观测误差,不是动画本身。