Animations Without Jank: Performance Tricks for Motion in Web Apps

Team 4 min read

#webperf

#frontend

#motion

Introduction

Motion should feel fluid, not chaotic. In web apps, jank often hides in the gaps between frames: layout thrash, forced paints, and expensive JavaScript keeping the main thread busy. The goal of this post is to lay out practical, battle-tested strategies to keep animations smooth while keeping your app responsive and accessible.

Core Principles

  • Keep work on the compositor thread whenever possible. Transform and opacity changes can be rasterized by the GPU without triggering layout or paint.
  • Avoid layout thrashing. Reading layout properties forces the browser to recalculate styles and reflow the entire tree.
  • Measure with intention. Always profile with real user conditions and a representative device set.
  • Respect user preferences. If a user enables prefers-reduced-motion, gracefully degrade animations.

CSS vs JS animation

  • Prefer CSS transitions and CSS animations for properties that are composited (transform, opacity, filter). They are usually optimized by the browser and run on the compositor.
  • Use JavaScript only when you need dynamic control, synchronization with data, or physics-based motion. When doing JS-driven animation, coordinate with requestAnimationFrame and keep per-frame work minimal.
  • Use will-change sparingly. It hints the browser about upcoming changes but can exhaust resources if overused.

Techniques to avoid jank

  • Animate only composited properties: transform and opacity are your friends. Avoid animating layout-affecting properties like width, height, margin, padding, top, left.
  • Use transform translateZ(0) or translate3d(0,0,0) to promote elements to the GPU layer when appropriate, but don’t overdo it.
  • Minimize paints. Complex box-shadows, heavy filters, or large paint areas can cause slower frame times.
  • Leverage containment. If a component updates independently, use contain: layout, paint, or content to limit the scope of recalculation.
  • Batch DOM reads and writes. Read all necessary values first, then apply changes. This reduces layout/reflow thrashing.
  • Debounce input-driven motion. For continuous input (dragging, scrolling), throttle or use passive event listeners to avoid blocking the main thread.
  • Respect reduced motion. Use media query prefers-reduced-motion to switch to simpler motion or static states.

Practical patterns

  • Pattern: hover or state-based movement

    • CSS approach:
      • Use transitions on transform and opacity.
      • Example:
        .card {
          transition: transform 200ms ease, opacity 200ms ease;
          will-change: transform, opacity;
        }
        .card:hover {
          transform: translateY(-4px);
          opacity: 0.98;
        }
    • Why it works: no layout recalculation; the browser can move the card via the compositor.
  • Pattern: scroll-linked animation

    • Prefer CSS for simple scroll-linked opacity or transform. For more complex behavior, fuse with a lightweight JS RAF loop that updates only a single numeric value and applies it to a transform.
    • Example (JS-based, with RAF):
      let lastY = 0;
      let ticking = false;
      const el = document.querySelector('.header');
      function onScroll() {
        lastY = window.scrollY;
        if (!ticking) {
          window.requestAnimationFrame(() => {
            el.style.transform = `translateY(${lastY * -0.2}px)`;
            ticking = false;
          });
          ticking = true;
        }
      }
      window.addEventListener('scroll', onScroll, { passive: true });
    • Why it works: updates are synced to the display refresh and avoid forcing reflows.
  • Pattern: dimensional animations inside a contained component

    • Use contain to isolate layout/paint for heavy widgets:
      .widget {
        contain: layout paint;
        will-change: transform;
        transform: translateZ(0);
      }
    • Why it works: limits the scope of repaints and layout recalculations when the widget updates.

Measuring and profiling

  • Use Chrome DevTools Performance tab to capture a representative user scenario. Look for:
    • Frame times consistently under 16ms for 60fps.
    • Long Tasks: operations that exceed ~50ms in a single frame.
    • Recalculate Style, Reflow, and Paint events.
  • Use the FPS indicator and painter’s optimizer to identify hotspots.
  • When possible, isolate animation to a small subtree so you can measure differences without global noise.

Accessibility considerations

  • Always respect prefers-reduced-motion. Provide a non-animated fallback for users who opt out of motion.
    • Example:
      @media (prefers-reduced-motion: reduce) {
        .card { transition: none; transform: none; opacity: 1; }
      }
  • Ensure focus indicators remain visible during animated transitions and that motion does not interfere with keyboard navigation.

Code examples: quick references

  • Avoiding layout thrash:
    // Bad: reading layout then writing in a loop
    const w = el.offsetWidth;
    el.style.width = w + 10 + 'px';
    
    // Good: batch reads, then writes
    const w = el.offsetWidth;
    requestAnimationFrame(() => {
      el.style.width = w + 10 + 'px';
    });
  • Smooth loader bar:
    .loader {
      width: 0;
      height: 4px;
      background: #06c;
      transition: width 250ms ease;
    }
    // Increase width based on progress, but only on transform/opacity
  • GPU-accelerated animation snippet:
    .floating-icon {
      transform: translate3d(0, 0, 0);
      animation: float 4s ease-in-out infinite;
    }
    @keyframes float {
      0%, 100% { transform: translateY(0); }
      50% { transform: translateY(-8px); }
    }

Conclusion

Butter smooth motion is less about fancy effects and more about disciplined engineering: keep work off the main thread, rely on compositor-friendly properties, measure under realistic conditions, and respect users’ motion preferences. By combining CSS for straightforward animations with small, well-timed JavaScript updates when necessary, you can deliver motion that enhances user experience without introducing jank.