Skip to main content

Profiling Animation Jank in Chrome DevTools

Animation jank is invisible in code—you detect it by watching and measuring. The Chrome DevTools Performance tab is your primary weapon: it records every frame, shows you exactly which steps (JavaScript, style, layout, paint, composite) took too long, and gives you frame-by-frame timing data. This article teaches you how to profile animations, identify bottlenecks, and confirm that your optimizations actually work.

Opening the Performance Tab

  1. Open Chrome DevTools (F12 on Windows/Linux, Cmd+Option+I on macOS).
  2. Click the Performance tab.
  3. Click the record button (circle icon) or press Ctrl+Shift+E (Windows/Linux) / Cmd+Shift+E (macOS).
  4. Interact with your animation (click a button, hover, scroll, etc.).
  5. Click the stop button or press the same shortcut to stop recording.

The DevTools will show a timeline of all rendering work.

Reading the Timeline

The Performance timeline has several rows:

RowWhat It Shows
NetworkNetwork requests (not relevant for most animations)
FramesGreen bars = 60 fps frame, red = dropped frame or frame that exceeded 16.67 ms
Main ThreadJavaScript, style, layout, paint on the main thread
GPUCompositing and rasterization work (may be empty if GPU rasterization is off)
RenderingPaint and composite operations

A green frame completed in under 16.67 ms (at 60 Hz). A red frame took longer and was skipped. Hovering over a frame shows its duration.

Analyzing Frame Timing

Click on a specific frame in the Frames row to see a detailed breakdown:

Frame: 16.7 ms (60 fps ✓)
├─ Rendering: 2.5 ms
│ ├─ Recalculate Style: 0.8 ms
│ ├─ Layout: 0.9 ms
│ ├─ Paint: 0.5 ms
│ └─ Composite: 0.3 ms
└─ JavaScript: 1.2 ms
├─ requestAnimationFrame: 1.0 ms
└─ (other): 0.2 ms

If Recalculate Style or Layout takes > 2 ms, you are likely animating layout-triggering properties. If Paint takes > 1 ms on a simple animation, you may be animating paint-triggering properties.

Enabling Paint Flashing

To visualize which regions are being repainted, enable paint flashing:

  1. In DevTools, click (three dots) → More toolsRendering.
  2. Check Paint flashing.

Now, as you interact with your page, regions that are repainted flash green. If your animation triggers flashing, you are not animating compositor-friendly properties.

Example: Bad Animation with Paint Flashing

// Bad: animates `color`, which triggers paint
export default function BadColorAnimation() {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
color: isHovered ? '#ff6b6b' : '#4ecdc4',
transition: 'color 0.3s ease',
}}
>
Hover me
</div>
);
}

When you hover over this, you will see the text region flash green (paint), indicating a repaint every frame.

Using the Rendering Tab

The Rendering tab (Tools → More tools → Rendering) has several useful options:

Show Compositing Borders

Displays green borders around composite layers. If your animated element has no green border, it is not on its own layer.

Show Paint Timing

Displays how long the paint step takes. If paint takes > 5 ms during animation, you are animating paint-triggering properties.

Disable Local Fonts

Simulates loading fonts over the network, which can interfere with animation timing. Disable local fonts to stress-test your animation on slow networks.

Frame Rate Analysis

The Frames row shows frame rate over time:

Frames: ████████░░████░░███████
^ Smooth ^ Dropped ^ Smooth

A solid line of green bars indicates consistent 60 fps. Gaps or red bars indicate jank. Count the red bars and divide by total frames to calculate jank percentage.

If you see jank during animation, zoom into that region of the timeline to see which rendering step is taking too long.

Identifying the Bottleneck

Here is a systematic approach to find the bottleneck:

1. Check Frames Row

  • All red frames? The entire animation is jank. Check step 2.
  • Intermittent red frames? Other work (events, React renders) is interfering. Check step 3.

2. Check Main Thread Timeline

Look at the breakdown of JavaScript, Style, Layout, Paint, Composite:

  • JavaScript spike? An event handler or animation callback is taking too long. Profile the JavaScript (see step 4).
  • Layout spike? Animating a layout-triggering property (left, top, width, height). Refactor to use transform.
  • Paint spike? Animating a paint-triggering property (color, background, border-radius). Refactor to use opacity.
  • Composite spike? Usually fast. If it is slow, you have too many elements or layers.

3. Check for Interference

If jank is intermittent, another task is blocking the main thread:

  • Scroll listener firing during animation? Debounce or throttle the listener.
  • React re-render during animation? Avoid state updates during animation; defer them until after.
  • Network request completing? Animations are often blocked by fetch completion handlers.

4. Profile JavaScript

If the JavaScript row shows a large spike, click on it to expand and see which functions are running:

requestAnimationFrame callback: 8.2 ms ⚠
├─ animate(): 7.5 ms
│ ├─ setTransform(): 0.1 ms
│ └─ expensiveCalculation(): 7.3 ms ⚠
└─ React render: 0.6 ms

If a function takes > 2 ms, optimize it or offload it to a Web Worker.

Measuring with the Console

Use performance.now() to measure animation timing in the console:

const startTime = performance.now();

element.animate([...], { duration: 1000 }).finished.then(() => {
const endTime = performance.now();
console.log(`Animation took ${endTime - startTime} ms`);
});

Run this in the DevTools console while profiling to correlate your timing with the timeline.

Creating a Performance Test Case

To diagnose animation problems, create a minimal test case that isolates the animation:

// Minimal test case: single animated element
export default function AnimationTest() {
const [isAnimating, setIsAnimating] = useState(false);

return (
<div>
<button onClick={() => setIsAnimating(!isAnimating)}>Animate</button>
{isAnimating && (
<div
style={{
width: '100px',
height: '100px',
backgroundColor: '#4ecdc4',
animation: 'slide 1s ease-out forwards',
}}
/>
)}
<style>{`
@keyframes slide {
from { transform: translateX(-100px); }
to { transform: translateX(0); }
}
`}</style>
</div>
);
}

Profile this minimal case. If it is smooth, the jank is caused by interactions with other parts of your page (DOM complexity, other scripts, etc.). If it is janky, the animation itself is the problem.

Benchmarking Different Properties

Compare animation performance across different properties:

// Compare transform (fast) vs left (slow)
export default function AnimationComparison() {
const [useTransform, setUseTransform] = useState(true);

return (
<div>
<button onClick={() => setUseTransform(!useTransform)}>
{useTransform ? 'Switch to `left`' : 'Switch to `transform`'}
</button>
<div
style={useTransform ? {
animation: 'slideTransform 1s ease-out infinite',
} : {
animation: 'slideLeft 1s ease-out infinite',
position: 'relative',
}}
/>
<style>{`
@keyframes slideTransform {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
@keyframes slideLeft {
from { left: 0; }
to { left: 100px; }
}
`}</style>
</div>
);
}

Profile both versions. You will see that transform maintains 60 fps while left drops frames.

Key Takeaways

  • Use Chrome DevTools Performance tab to record and analyze animation timing frame-by-frame.
  • Green frames = under 16.67 ms (60 fps); red frames = over 16.67 ms (dropped frame).
  • Enable paint flashing to visualize repaints during animation; no flashing indicates compositor-only animation.
  • Identify bottlenecks by examining the Main Thread timeline: JavaScript spike → profile functions; Layout spike → refactor to transform; Paint spike → refactor to opacity.
  • Create minimal test cases to isolate animation performance from page complexity.

Frequently Asked Questions

What is a good frame rate to aim for?

Aim for 60 fps on standard displays (16.67 ms per frame) and 120 fps on high-refresh displays (8.33 ms per frame). If you cannot maintain 60 fps, the animation feels janky. Aim for consistent frame rate over high peak performance.

Why does my animation have intermittent jank every few frames?

This usually indicates that another task (event handler, network completion, React render) is firing on the main thread. Use the timeline to see what is running at the moment of jank. Debounce event handlers, avoid state updates during animations, or move heavy computation to a Web Worker.

How do I profile animations that run in the background (not triggered by user interaction)?

Start the Performance recording, wait for the animation to begin, then stop recording. Or, trigger the animation yourself (click a button) during recording to ensure the DevTools captures the entire animation lifecycle.

Can I profile animations on mobile devices?

Yes. Use Chrome Remote Debugging to connect a mobile device to your development machine, then use the desktop DevTools to profile the mobile browser. The timeline will show the mobile device's performance characteristics (slower CPU, GPU).

Why does DevTools show 60 fps, but the animation still feels janky on my monitor?

DevTools measures frame timing, not visual smoothness. If you are using a 120 Hz or 144 Hz monitor but your animation targets 60 fps, the animation may appear janky relative to the monitor's refresh rate. Target 120 fps on high-refresh displays or use techniques like motion prediction to smooth lower-fps animations.

Is there a way to simulate a slow device in DevTools?

Yes. In the DevTools Settings → Throttling, select "4x slowdown" (CPU) to simulate a slower device. Profiling with throttling enabled shows how your animation performs on budget devices.

Further Reading