Learn how to connect Three.js with GSAP ScrollTrigger in React to build 60 FPS scroll-driven 3D product showcases without performance drops.
Adding 3D models to web applications often leads to choppy scrolling, drained laptop batteries, and janky mobile performance. Many developers attempt to synchronize Three.js scenes with the DOM by storing window scroll positions in React state or context. When state updates 60 times a second, React reconciles the virtual DOM on every frame, starving the GPU and dropping render rates to unplayable levels.
To build an effective interactive 3D product showcase, WebGL and DOM scroll tracking must operate on separate tracks. React mounts the canvas and sets up the component lifecycle, while GSAP ScrollTrigger directly writes transformations to Three.js mesh instances without re-rendering the React tree.
When building the interactive product view for the [3D Burger Website / Smoked & Seared](/projects/13), the goal was to disassemble individual ingredients—top bun, sesame seeds, cheese, patty, and bottom bun—as the visitor scrolls past distinct nutritional sections. Instead of re-rendering, we target the position, rotation, and scale of child meshes directly using GSAP timelines.
import React, { useRef } from 'react';
import { Canvas } from '@react-three/fiber';
import { useGLTF } from '@react-three/drei';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useGSAP } from '@gsap/react';
import * as THREE from 'three';
gsap.registerPlugin(ScrollTrigger);
interface BurgerModelProps {
containerRef: React.RefObject<HTMLDivElement | null>;
}
function BurgerModel({ containerRef }: BurgerModelProps) {
const groupRef = useRef<THREE.Group>(null);
const topBunRef = useRef<THREE.Mesh>(null);
const pattyRef = useRef<THREE.Mesh>(null);
const bottomBunRef = useRef<THREE.Mesh>(null);
const { nodes, materials } = useGLTF('/models/burger.glb') as any;
useGSAP(
() => {
if (!groupRef.current || !containerRef.current) return;
const tl = gsap.timeline({
scrollTrigger: {
trigger: containerRef.current,
start: 'top top',
end: 'bottom bottom',
scrub: 1,
},
});
// Rotate the entire product during scroll
tl.to(groupRef.current.rotation, { y: Math.PI * 2, ease: 'none' }, 0);
// Explode layers vertically at specific scroll markers
if (topBunRef.current) {
tl.to(topBunRef.current.position, { y: 2.2, ease: 'power1.out' }, 0.2);
}
if (pattyRef.current) {
tl.to(pattyRef.current.position, { y: 0.5, ease: 'power1.out' }, 0.3);
}
if (bottomBunRef.current) {
tl.to(bottomBunRef.current.position, { y: -1.2, ease: 'power1.out' }, 0.2);
}
},
{ scope: containerRef }
);
return (
<group ref={groupRef} dispose={null}>
<mesh ref={topBunRef} geometry={nodes.TopBun.geometry} material={materials.Bun} />
<mesh ref={pattyRef} geometry={nodes.Patty.geometry} material={materials.Meat} />
<mesh ref={bottomBunRef} geometry={nodes.BottomBun.geometry} material={materials.Bun} />
</group>
);
}
export default function ProductShowcase() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef} className="relative h-[300vh] bg-stone-900">
<div className="sticky top-0 h-screen w-full">
<Canvas
dpr={[1, 2]}
camera={{ position: [0, 0, 5], fov: 45 }}
gl={{ antialias: true, powerPreference: 'high-performance' }}
>
<ambientLight intensity={0.7} />
<directionalLight position={[10, 10, 5]} intensity={1.2} />
<BurgerModel containerRef={containerRef} />
</Canvas>
</div>
</div>
);
}
Interactive 3D can quickly overwhelm mobile GPUs if default canvas settings are left untouched. To guarantee a stable 60 FPS across mid-range phones and high-resolution displays like Apple Retina screens, follow these production rules:
For luxury travel experiences like [Yacht Rentals / Sea Cross](/projects/10), pairing smooth scroll libraries like Lenis with GSAP ScrollTrigger ensures the camera and mesh interpolations match human finger swipes without stutter.
If you want to replace static product photography with interactive 3D web experiences that run smoothly across desktop and mobile devices, I can help design and build the front-end architecture. Check out my [Full-Stack Web Development Services](/services) or [get in touch with Aafaque](/contact) to discuss your project requirements.
Wrap your animations inside the useGSAP hook from @gsap/react or use gsap.context() inside a standard useEffect. This records all created triggers and automatically reverts them when the component unmounts, preventing duplicate pins and memory leaks.
No. Storing scroll progress in React state triggers component re-renders on every scroll tick, causing severe frame drops. Instead, mutate Three.js mesh transforms directly via GSAP tweens using direct object references.
Cap the device pixel ratio at Math.min(window.devicePixelRatio, 2), compress GLTF models using Draco or Meshopt, bake complex lighting into textures to reduce draw calls, and pause the Three.js render loop when the canvas scrolls out of the viewport.