Lessons from building MERN ERPs
Production status, a separate .NET PDF service, user-based S3 organization, and the boundaries of my ERP work.
When pairing virtual smooth scroll libraries like Lenis with GSAP ScrollTrigger animations, a subtle but persistent stutter frequently appears on high-refresh-rate displays. The root cause is dual RequestAnimationFrame (RAF) loops running out of phase: Lenis computes scroll positions on its own loop, while GSAP triggers layout measurements on another. The result is micro-jitter, sub-pixel displacement errors, and redundant layout passes.
The definitive architecture binds Lenis's internal update pump directly to GSAP's central RAF loop (the master ticker). By turning off Lenis's automatic RAF cycle (autoRaf: false) and driving its step calculations exclusively via gsap.ticker.add, both libraries advance on the exact same frame boundary.
import { useEffect, useRef } from "react";
import { ReactLenis } from "lenis/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
export default function SmoothScrollProvider({ children }) {
const lenisRef = useRef(null);
useEffect(() => {
const lenis = lenisRef.current?.lenis;
if (lenis) {
lenis.on("scroll", ScrollTrigger.update);
}
// Drive Lenis strictly through GSAP's internal delta ticker
const update = (time) => {
lenisRef.current?.lenis?.raf(time * 1000);
};
gsap.ticker.add(update);
gsap.ticker.lagSmoothing(0); // Prevents scroll jumps during CPU spikes
return () => {
if (lenis) lenis.off("scroll", ScrollTrigger.update);
gsap.ticker.remove(update);
};
}, []);
return (
<ReactLenis ref={lenisRef} root autoRaf={false} options={{ lerp: 0.1, duration: 1.2 }}>
{children}
</ReactLenis>
);
}
By default, GSAP includes lag smoothing: if heavy computation causes a frame to take longer than 500ms, GSAP throttles the delta time to avoid sudden animation teleportation. However, with virtual smooth scrolling, lag smoothing causes a noticeable desynchronization where the virtual scroll position "drifts" behind the physical wheel event. Setting gsap.ticker.lagSmoothing(0) disables artificial clock adjustments, ensuring instantaneous and predictable coordinate matching.
Notice the cleanup return inside useEffect. When navigating across routes in Next.js App Router, unmounting the provider without unbinding gsap.ticker.remove(update) leaves ghost ticker handlers running in memory. The return hook ensures complete garbage collection and prevents duplicate tickers from stacking on client transitions.
Production status, a separate .NET PDF service, user-based S3 organization, and the boundaries of my ERP work.
JSON templates, browser rendering, and remaining-space pagination in my client-side PDF generator.