Lenis makes scrolling feel expensive. GSAP's ScrollTrigger makes scrolling do things. Run both naively and they each run their own requestAnimationFrame loop, disagree about scroll position mid-frame, and produce the exact jank you installed them to avoid.
The pattern
Drive Lenis from GSAP's ticker instead of its own loop: gsap.ticker.add((t) => lenis.raf(t * 1000)). Tell ScrollTrigger about every Lenis scroll: lenis.on('scroll', ScrollTrigger.update). Disable GSAP's lag smoothing so the two never drift: gsap.ticker.lagSmoothing(0). One loop, one source of truth, zero fights.
Where smooth scroll doesn't belong
Touch devices already have physics-based native scrolling that users trust. A JS scroll layer on mobile costs main-thread time and often feels worse. Gate Lenis behind a (hover: hover) and (pointer: fine) media query — desktops get the glide, phones keep their native feel and their battery.