无头截图永远看到动画末态?四个混淆因素逐个击破
写了一段 0.8s 揭幕 + 1.2s 渐出的一次性进场动画,打开真机一切正常。切换到 Playwright / chrome-devtools-mcp 截图验证,每次看到的都是「满屏内容全部揭开」,像动画根本没播—— 于是开始反复改代码,空转二十轮,越改越乱。
最后发现:动画从没出过问题,是观测本身一直落在动画结束之后。
现象
- 每次
navigatereload →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 的早段,
让「中段」视觉上仍保留明显的未完成状态。
# 验证期临时配置
COVER_MS=12000 # 远超任何工具延迟
EASE=linear # 中段依然可见
截图看到满幕遮罩 = 渲染 OK,之前是观测落点问题,不是代码问题。
再用 evaluate 采样 canvas/overlay 像素值做二次确认,比纯截图更可靠。
实测区分手法
| 查询方式 | 能区分什么 |
|---------|-----------|
| evaluate 查挂载标志 | 组件有没有挂上去 |
| evaluate 查门控 flag 值 | 动画是被跳过还是真的播了 |
| evaluate 采样 canvas/overlay 像素 | 当前帧是遮罩色还是背景色 |
| 纯截图 | 无法区分「没渲染」和「已结束」|
一句话外卖
无头截图验证亚秒动画前,先把时长拉到 10s+ 排除观测落点问题;否则你调的是观测误差,不是动画本身。