| name | optimize-web-animations |
| description | 剖析、审计并优化前端页面性能,重点关注动画工作、内存泄漏风险、长会话变慢、CSS 动画、canvas/WebGL 的 requestAnimationFrame 循环、marquee、skeleton、GSAP/Three/Matter 效果、定时器、监听器和 observer。当用户要求让动画高性能、暂停离屏动画、查找内存泄漏、剖析随时间拖慢电脑的页面、修复卡顿滚动、降低 CPU/GPU 占用,或在 React/Vite/Next/前端页面上用 Codex Browser 复用"仅在视野内播放"优化时使用。 |
优化 Web 动画
核心规则
编辑前先测量真实页面。目标不是移除动效;而是让离屏工作停止、让可见动效正确恢复,并让路由/卸载清理释放长期存活的资源。
尽可能使用 Codex Browser,尤其对 localhost 页面。除非用户明确要求,否则不要用 Chrome。
工作流程
-
检查仓库上下文。
- 阅读
AGENTS.md 或本地指令。
- 尽早运行
git status --short。
- 找到页面组件、动画 hook、CSS keyframes、
requestAnimationFrame、setInterval、setTimeout、canvas/WebGL/物理组件、媒体元素、GSAP 时间线/tween,以及已有的可见性工具。
- 搜索 effect 清理中的事件监听器、observer、RAF 循环、interval、定时器、外部脚本、媒体流、WebGL 纹理/材质/几何体/渲染器,以及可能在卸载后完成的异步工作。
- 如果工作树是脏的,从一开始就规划窄范围的暂存。
-
在浏览器中采集基线。
- 打开用户点名的确切路由。
- 在顶部、页面中部、页脚/靠下内容处剖析,并在布局可能不同时加一个移动端视口。
- 按计算得到的
animationName、animationPlayState 和可见性统计 CSS 动画。包含 ::before 和 ::after。
- 单独检查 canvas/WebGL 元素;CSS 剖析并不能证明 RAF 循环已停止。
- 记录哪些动画名在离屏运行,以及负责它们的 DOM 所有者。
- 对内存/泄漏问题,还要记录 element/canvas/image/iframe 计数、可用时暴露的 JS 堆指标、10-30 秒后的一个空闲采样,以及一段短的路由循环采样。如果堆 API 返回
null 或 Browser 沙箱阻止 monkey-patching,就说明这点,并依赖稳定的可观测计数加上源码审计。
- 让压力测试保持有界。剖析期间 Browser 标签页崩溃是过载的证据,但除非用最小测试复现,否则不要过度归因。
- 可复用的 Codex Browser 求值器见
references/browser-profiling.md。
-
给控制该动效的最小所有者打补丁。
- 如果应用有已存在的页面 reveal/可见性 hook,优先用它。
- 否则加一个
IntersectionObserver,在板块和被动画的子元素上切换一个稳定的类,如 is-offscreen。
- 用有针对性的规则暂停 CSS 动画:
main > section.is-offscreen .expensive-animation,
.expensive-animation.is-offscreen {
animation-play-state: paused !important;
}
- 对重复的卡片或占位符,观察卡片外壳和被动画的后代元素,而不是整个文档。
- 对 marquee/ticker 轨道,当其板块离屏时暂停轨道。
- 对 skeleton 加载器和伪元素微光,在需要处包含
::before 和 ::after 的暂停选择器。
- 对 canvas/WebGL/物理循环,直接闸控 RAF 循环:
- 当 canvas/容器相交时启动。
- 离屏时取消
requestAnimationFrame。
- 重新进入时恢复。
- 清理时断开 observer 并取消帧。
- 当有助于浏览器校验时,加一个非视觉的调试标记如
data-animation-active。
- 如果组件已经这样做了,尊重
prefers-reduced-motion,并避免为滚动/动画状态引入 React 渲染循环。
- 对泄漏加固:
- 清除该 effect 创建的每个 timeout/interval。
- 在卸载前以及重启循环前取消 RAF。
- 断开
IntersectionObserver、ResizeObserver、MutationObserver 和自定义订阅。
- 用相同的 handler 引用移除 global/window/document 监听器。
- 释放 Three/WebGL 的纹理、材质、几何体、渲染器,并移除渲染器 DOM 节点。
- 杀掉针对 DOM 节点和可变对象(如 shader uniform)的 GSAP tween/时间线。
- 停止媒体流,暂停已分离的 video/audio 源。
- 用一个
isDisposed 标志守护异步加载器,如果它们在卸载后 resolve,就释放已加载的资源。
- 在 React 清理中,如果 lint 警告 ref 可能变化,在返回清理函数前于 effect 内捕获
ref.current 的值。
- 在可见性暂停后为物理或模拟帧的 delta 封顶,使被延迟的帧不会运行过大的更新。
-
校验行为,而不只是构建。
- 重新加载路由并重跑相同的 顶部/中部/页脚/移动端 剖析。
- 目标结果:被测页面板块的
offscreenRunningCount: 0。
- 确认可见动画在滚入视野时仍在运行或恢复。
- 确认 RAF/canvas 循环在离屏时报告非活动、在视野内报告活动,或以其他方式从源码/运行时状态证明取消。
- 对泄漏审计,对比前后的路由循环和空闲采样。反复导航后,DOM/canvas/image 计数应回到相同基线,允许预期内的小幅异步内容变化。
- 执行一次正常的页面交互,如搜索/筛选/导航,使 observer 不会破坏动态内容。
- 检查全新标签页的 console 警告/错误。
-
运行本地检查。
- 使用仓库的常规关卡。对 React/Vite 应用通常是:
git diff --check
npm run lint
npm run build
- 在仓库/用户指令要求时窄范围提交。
- 如果存在不相关的脏改动,使用一个隔离的 index:
rm -f /tmp/<task>-index
GIT_INDEX_FILE=/tmp/<task>-index git read-tree HEAD
GIT_INDEX_FILE=/tmp/<task>-index git diff --cached --check
GIT_INDEX_FILE=/tmp/<task>-index git commit -m "Pause offscreen <page> animations"
git restore --staged <files> 2>/dev/null || true
- 除非每个 hunk 都属于本任务,否则绝不从脏工作树暂存大范围文件。
- 带证据报告。
- 以发现开头:什么还在运行、什么看起来易泄漏、什么无法测量。
- 把源码审计风险与 live Browser 测量分开。
- 包含确切采样的路由、离屏动画计数、DOM/canvas 计数稳定性、路由循环结果,以及本地检查。
- 直白地说明局限,尤其是不可用的堆计数器或被阻塞的 Browser 插桩。
好的修复模式
- 板块级
is-offscreen 加元素级 is-offscreen,用于那些首屏以下子动画仍可能运行的长板块。
- 每路由共享的可见性选择器常量,如
COURSES_PAGE_ANIMATION_VISIBILITY_SELECTOR。
- 动画闸控用
IntersectionObserver,阈值约 0.01。
- 对 WebGL/canvas 效果直接控制 RAF 循环;CSS
animation-play-state 无法暂停 JavaScript 渲染循环。
- 对在暂停或延迟帧后恢复的物理循环,做帧 delta 封顶。
- 为 GSAP/WebGL 效果使用的 React ref 捕获清理节点。
- 为可能在卸载后 resolve 的 image/video/texture/data 加载器加
isDisposed 守护。
- 用短的空闲和路由循环探针,捕捉累积的 DOM 节点、canvas、iframe 或未释放的媒体。
避免
- 为让剖析通过而移除所有动画。
- 因为祖先选择器太宽泛而暂停了可见的 hero 动效。
- 假设
animation-play-state 覆盖伪元素或 JavaScript RAF 循环。
- 在长页面上只信一次页顶测量。
- 把不可用的堆计数器当作没有内存泄漏的证据。
- 在 Browser 中运行无界的压力循环;用有界循环,并在不夸大因果的前提下记录崩溃。
- 只用截图作为性能证据。
- 让不相关的本地 hunk 搭车进入提交。