[ANIMATION]// 2026-02-18// 4 min read

GSAP ScrollTrigger Lifecycle & Cleanup in React 19 / Next.js

How to safely orchestrate pinned scroll animations, smooth scrolling (Lenis), and client-side page transitions without memory leaks or layout thrashing.

#GSAP#React 19#Next.js#ScrollTrigger#Performance

The Core Problem: Zombie ScrollTriggers

When orchestrating complex pinned animations in single-page applications, route transitions frequently leave dangling ScrollTrigger instances attached to DOM nodes that no longer exist. In React 19, strict concurrency and component lifecycle timing make manual tween tracking prone to race conditions.

The Solution: gsap.context() as a Lifetime Boundary

By wrapping every timeline and trigger inside `gsap.context()`, you establish an isolated execution boundary. Calling `ctx.revert()` in the cleanup callback of your `useEffect` guarantees that all child timelines are stopped, inline styles applied by GSAP are rolled back to their initial state, and ScrollTriggers are unregistered.

LANG // TYPESCRIPTStrict gsap.context() encapsulation in React client components.
useEffect(() => {
  const ctx = gsap.context(() => {
    // All selectors inside are automatically scoped
    gsap.to(".anim-target", {
      scrollTrigger: {
        trigger: "#section",
        start: "top top",
        end: "+=100%",
        scrub: true,
        pin: true,
      },
      scale: 1.2,
      opacity: 0,
    });
  });

  return () => {
    // Reverts inline styles and unregisters all ScrollTriggers cleanly
    ctx.revert();
  };
}, []);

Coordinating Lenis and ScrollTrigger

When integrating smooth scrolling via Lenis, never let Lenis and GSAP run separate animation frames. Wire Lenis into GSAP's central ticker so that scroll position updates and timeline calculations execute synchronously in the exact same microtask tick.

LANG // TYPESCRIPTSynchronizing Lenis RAF with GSAP's central ticker.
const lenis = new Lenis({ lerp: 0.08 });
lenis.on("scroll", ScrollTrigger.update);

const tickerCallback = (time: number) => {
  lenis.raf(time * 1000);
};

gsap.ticker.add(tickerCallback);
gsap.ticker.lagSmoothing(0);
CORE TAKEAWAY

Always disable custom smooth scrolling on touch devices (`navigator.maxTouchPoints > 0`). Native inertial scrolling on iOS and Android provides superior tactile responsiveness.

Mobile Viewport Resizing (Address Bar Collapse)

Mobile WebKit resizes the inner height as the user scrolls, triggering ScrollTrigger recalculations that cause jarring visual stutter. Call `ScrollTrigger.normalizeScroll(true)` on touch devices to lock scroll containment and eliminate address bar jitter.