Imported from akillness/jeo-skills (
.agent-skills/optimize-web-animations/SKILL.md). Install upstream withnpx skills add akillness/jeo-skills --skill optimize-web-animations. Copyright stays with the author (MIT).
Optimize Web Animations
When to use this skill
- The user asks to make animations performant, pause offscreen animations, or repeat the "only play in view" optimization on a React/Vite/Next/frontend page.
- The user reports memory leaks, pages that slow down the computer over a long session, or wants long-lived resources (RAF loops, listeners, observers, timers, GSAP tweens, WebGL objects) audited and released.
- The user wants janky scrolling fixed or CPU/GPU use reduced from CSS animations, canvas/WebGL
requestAnimationFrameloops, marquees/tickers, skeleton loaders, or GSAP/Three/Matter effects. - The work requires a measured baseline-then-fix loop in the browser (Codex Browser preferred), gating motion with
IntersectionObserver/is-offscreenclasses, and verifying withoffscreenRunningCount, DOM/canvas counts, and route-cycle stability rather than screenshots alone. - The fix needs source-level cleanup discipline: clearing timers, cancelling RAF, disconnecting observers, removing listeners, and disposing Three/WebGL/GSAP resources on unmount.
- Not for general performance-artifact triage (deciding what a trace, query plan, load-test result, or CWV/Lighthouse report means, or which bottleneck to tune first across latency/throughput/memory/bundle-size) — route that to
performance-optimizationinstead; use this skill only once the target is already known to be animation/motion-related frontend work.
Core Rule
Measure the real page before editing. The goal is not to remove motion; it is to make offscreen work stop, visible motion resume correctly, and route/unmount cleanup release long-lived resources.
Use Codex Browser when available, especially for localhost pages. Do not use Chrome unless the user explicitly asks for it.
Workflow
-
Inspect repo context.
- Read
AGENTS.mdor local instructions. - Run
git status --shortearly. - Find page components, animation hooks, CSS keyframes,
requestAnimationFrame,setInterval,setTimeout, canvas/WebGL/physics components, media elements, GSAP timelines/tweens, and existing visibility utilities. - Search effect cleanup for event listeners, observers, RAF loops, intervals, timers, external scripts, media streams, WebGL textures/materials/geometries/renderers, and async work that can complete after unmount.
- If the worktree is dirty, plan narrow staging from the start.
- Read
-
Capture a baseline in the browser.
- Open the exact route the user named.
- Profile at top, mid-page, footer/lower content, and one mobile viewport when layout could differ.
- Count CSS animations by computed
animationName,animationPlayState, and visibility. Include::beforeand::after. - Inspect canvases/WebGL elements separately; CSS profiling does not prove RAF loops have stopped.
- Record which animation names are running offscreen and the DOM owners responsible.
- For memory/leak asks, also record element/canvas/image/iframe counts, exposed JS heap metrics when available, an idle sample after 10-30 seconds, and a short route-cycle sample. If heap APIs return
nullor the Browser sandbox blocks monkey-patching, say so and rely on stable observable counts plus source audit. - Keep stress tests bounded. A Browser tab crash during profiling is evidence of overload, but do not over-attribute the cause unless reproduced by a minimal test.
- See
references/browser-profiling.mdfor a reusable Codex Browser evaluator.
-
Patch the smallest owner that controls the motion.
- Prefer an existing page reveal/visibility hook if the app has one.
- Otherwise add an
IntersectionObserverthat toggles a stable class such asis-offscreenon sections and animated child elements. - Pause CSS animations with targeted rules:
main > section.is-offscreen .expensive-animation,
.expensive-animation.is-offscreen {
animation-play-state: paused !important;
}
- For repeated cards or placeholders, observe the card shell and the animated descendants, not the whole document.
- For marquee/ticker tracks, pause the track when its section is offscreen.
- For skeleton loaders and pseudo-element glimmers, include
::beforeand::afterpause selectors where needed. - For canvas/WebGL/physics loops, gate the RAF loop directly:
- Start when the canvas/container intersects.
- Cancel
requestAnimationFramewhen offscreen. - Resume on re-entry.
- Disconnect observers and cancel frames on cleanup.
- Add a non-visual debug marker such as
data-animation-activewhen it helps browser verification.
- Respect
prefers-reduced-motionif the component already does, and avoid introducing React render loops for scroll/animation state. - For leak hardening:
- Clear every timeout/interval created by the effect.
- Cancel RAF before unmount and before restarting a loop.
- Disconnect
IntersectionObserver,ResizeObserver,MutationObserver, and custom subscriptions. - Remove global/window/document listeners with the same handler reference.
- Dispose Three/WebGL textures, materials, geometries, renderers, and remove renderer DOM nodes.
- Kill GSAP tweens/timelines for DOM nodes and mutable objects such as shader uniforms.
- Stop media streams and pause detached video/audio sources.
- Guard async loaders with an
isDisposedflag and dispose loaded resources if they resolve after unmount. - In React cleanup, capture
ref.currentvalues inside the effect before returning cleanup if lint warns the ref may change. - Cap physics or simulation frame deltas after visibility pauses so delayed frames do not run oversized updates.
-
Verify behavior, not just builds.
- Reload the route and rerun the same top/mid/footer/mobile profiles.
- Target result:
offscreenRunningCount: 0for the page sections under test. - Confirm visible animations still run or resume when scrolled into view.
- Confirm RAF/canvas loops report inactive offscreen and active in view, or otherwise prove cancellation from source/runtime state.
- For leak audits, compare before/after route cycles and idle samples. DOM/canvas/image counts should return to the same baseline after repeated navigation, allowing for small expected async content changes.
- Exercise a normal page interaction such as search/filter/navigation so the observer does not break dynamic content.
- Check fresh-tab console warnings/errors.
-
Run local checks.
- Use the repo's normal gates. For React/Vite apps this is often:
git diff --check
npm run lint
npm run build
- Mention known non-fatal warnings separately from failures.
- Commit narrowly when requested by repo/user instructions.
- If unrelated dirty changes exist, use an isolated index:
rm -f /tmp/<task>-index
GIT_INDEX_FILE=/tmp/<task>-index git read-tree HEAD
# Apply only the intended hunks to the temporary index.
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
- Never stage broad files from a dirty worktree unless every hunk belongs to the task.
- Report with evidence.
- Lead with findings: what was still running, what looked leak-prone, and what could not be measured.
- Separate source-audit risks from live Browser measurements.
- Include the exact sampled route(s), offscreen animation counts, DOM/canvas count stability, route-cycle result, and local checks.
- State limitations plainly, especially unavailable heap counters or blocked Browser instrumentation.
Good Fix Patterns
- Section-level
is-offscreenplus element-levelis-offscreenfor long sections where below-the-fold child animations can still run. - Shared visibility selector constants per route, such as
COURSES_PAGE_ANIMATION_VISIBILITY_SELECTOR. IntersectionObserverthresholds around0.01for animation gating.- Direct RAF loop control for WebGL/canvas effects; CSS
animation-play-statecannot pause JavaScript render loops. - Frame delta caps for physics loops that resume after a paused or delayed frame.
- Captured cleanup nodes for React refs used by GSAP/WebGL effects.
isDisposedguards for image/video/texture/data loaders that may resolve after unmount.- Short idle and route-cycle probes to catch accumulating DOM nodes, canvases, iframes, or unreleased media.
Avoid
- Removing all animations to make the profile pass.
- Pausing visible hero motion because an ancestor selector is too broad.
- Assuming
animation-play-statecovers pseudo-elements or JavaScript RAF loops. - Trusting a single top-of-page measurement on long pages.
- Treating unavailable heap counters as proof there is no memory leak.
- Running unbounded stress loops in the Browser; use bounded cycles and record crashes without overstating causality.
- Using screenshots alone as performance proof.
- Letting unrelated local hunks ride along in the commit.
References
- Upstream source: https://github.com/MengTo/Skills/tree/main/agent-skills/codex/optimize-web-animations
- references/browser-profiling.md — reusable Codex Browser evaluator for offscreen/animation profiling
- agents/openai.yaml — bundled agent configuration