| name | animation-performance-auditor |
| description | This skill should be used when the user's animations feel slow, choppy, or frame-drop. Trigger when the user mentions "animation is janky", "animation is choppy", "dropped frames", "stuttering animation", "animation lags", "smooth on desktop but janky on mobile", "animation causes layout shifts", "scroll is not smooth", "60fps fail", "animation slows down page", "will-change", "GPU acceleration", "how to profile animation", "DevTools performance panel", "Chrome performance tab", or asks why their animation feels slow despite looking correct in code. Also trigger when the user asks about optimizing or auditing existing animations for performance. |
Animation Performance Auditor
Jank is the single most damaging animation problem: a choppy animation is worse than no animation. It signals broken software. This skill provides a systematic diagnostic workflow — from identifying the root cause to applying targeted fixes.
The core fact: At 60fps, every frame has a budget of 16.67ms. Any work that exceeds this budget drops a frame. Work that runs on the GPU (compositing) is essentially free; work that reaches Layout is the most expensive. The browser's rendering pipeline is:
JavaScript → Style Calculation → Layout → Paint → Composite
Animations that stay in Composite never block this pipeline. Any animation that reaches Layout must recalculate every affected element's position and size before painting — this is why top/left animation drops frames and transform: translate() does not.
Step 1: Identify If a Performance Problem Exists
Open Chrome DevTools → Performance panel:
- Click Record (⏺)
- Interact with the animated element
- Stop recording
- Look for red bars at the top of the timeline — these are dropped frames (frames that took > 16.67ms)
- Click on a red bar to see which phase took the longest (Scripting, Rendering, Painting)
If no red bars: the animation is performing correctly — the problem may be design, not performance.
Shortcut check without profiling:
Open DevTools → More Tools → Rendering → Enable:
- Frame Rendering Stats — live FPS counter
- Paint Flashing — green flashes on every repaint (tells you which elements repaint on each frame)
- Layout Shift Regions — blue highlights on layout shifts
If paint flashing shows large green regions during animation: something is triggering paint that should be composited.
Step 2: Identify the Problematic Property
Match the property being animated to its pipeline cost:
| Property | Pipeline stage | Cost | Fix |
|---|
transform: translate() | Composite | Free ✓ | Already optimal |
transform: scale() | Composite | Free ✓ | Already optimal |
transform: rotate() | Composite | Free ✓ | Already optimal |
opacity | Composite | Free ✓ | Already optimal |
filter: blur() | Composite (usually) | Low | Test, usually fine |
background-color | Paint | Medium | Accept for slow transitions; avoid during scroll |
border-radius (animated) | Paint | Medium | Use clip-path instead for better perf |
box-shadow (animated) | Paint | High | Use pseudo-element trick (see below) |
width, height | Layout | Very high | Replace with transform: scale() |
top, left, right, bottom | Layout | Very high | Replace with transform: translate() |
margin, padding | Layout | Very high | Use transform-based layout instead |
font-size | Layout | Very high | Avoid animating |
Step 3: Apply the Standard Fixes
Fix: Replace top/left with transform: translate()
.element {
position: absolute;
transition: top 300ms, left 300ms;
top: 20px; left: 0;
}
.element.active { top: 0; left: 20px; }
.element {
position: absolute;
transition: transform 300ms ease-out;
}
.element.active { transform: translate(20px, -20px); }
Fix: Replace animated box-shadow with a pseudo-element
.card {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: box-shadow 200ms;
}
.card:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.15); }
.card { position: relative; }
.card::after {
content: "";
position: absolute;
inset: 0;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
opacity: 0;
transition: opacity 200ms ease-out;
border-radius: inherit;
}
.card:hover::after { opacity: 1; }
Fix: Remove transition: all
.element { transition: all 300ms; }
.element {
transition:
transform 300ms ease-out,
opacity 200ms ease-out;
}
Fix: Add will-change for elements that animate repeatedly
.animated-card {
will-change: transform;
}
Warning: Do not add will-change to everything. It consumes VRAM. If the browser has too many composited layers, it runs out of GPU memory and performs worse. Use it on:
- Elements that animate on every interaction (hover, drag)
- Elements in a scroll-linked animation
- Elements in a looping animation
Never use it as a blanket optimization.
Fix: Contain layout recalc scope
.animation-container {
contain: layout style paint;
}
Step 4: Check for Layout Thrashing in JavaScript
Layout thrashing happens when JavaScript reads and writes layout properties in an interleaved loop, forcing the browser to recalculate layout synchronously on every iteration.
elements.forEach((el) => {
const height = el.offsetHeight;
el.style.height = height * 2 + "px";
});
const heights = elements.map((el) => el.offsetHeight);
elements.forEach((el, i) => {
el.style.height = heights[i] * 2 + "px";
});
In Chrome DevTools, layout thrashing appears as interleaved purple (Recalculate Style) and green (Layout) blocks in the flame chart, repeating many times within a single frame.
Use requestAnimationFrame for JS-driven animations:
setInterval(() => updateAnimation(), 16);
function tick(timestamp) {
updateAnimation(timestamp);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
Step 5: Check the Layers Panel
DevTools → More Tools → Layers:
- Each rectangle is a compositing layer on the GPU
- Correct: elements with
will-change: transform or position: fixed have their own layer
- Incorrect: hundreds of layers (suggests
will-change overuse or unintentional layer promotion from transform: translateZ(0) hacks)
View layer reasons: Click any layer to see why it was promoted. "Has a 3D transform" and "will-change: transform" are expected. Long lists of unexpected promotions signal a problem.
Mobile-Specific Considerations
Mobile GPUs are significantly weaker than desktop. An animation that runs at 60fps on desktop may drop to 30fps on a mid-range Android phone.
- Always test on real hardware (not just desktop DevTools mobile simulation)
filter: blur() is GPU-composited on desktop but triggers paint on some Android WebViews — test before committing
- Reduce amplitude (translate distance, scale range) on small screens — smaller devices move the same pixel count but it covers more of the screen proportionally
- Avoid simultaneously animating > 5 elements on mobile
Additional Resources
references/devtools-profiling.md — Full walkthrough of Chrome Performance panel, flame chart reading, identifying scripting vs. rendering vs. painting bottlenecks, Firefox equivalent tools, mobile remote debugging setup