Integrating Three.js and GSAP ScrollTrigger in React for 3D Product Showcases

Learn how to connect Three.js with GSAP ScrollTrigger in React to build 60 FPS scroll-driven 3D product showcases without performance drops.

Published by Aafaque Nazir on 2026-11-11 • 7 min read
Quick Answer: To sync Three.js with GSAP ScrollTrigger in React, update 3D mesh properties directly inside a GSAP timeline tied to scroll progress and clean up the timeline within a useGSAP or useEffect hook. Limit canvas pixel ratio to 2 and run animations outside React state to maintain a steady 60 frames per second.

The Problem with Traditional Scroll-Driven 3D in React

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.

Building the Bridge Between GSAP and Three.js

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>
  );
}
Clean Scope Management
Always pass scope to useGSAP. This ensures all ScrollTrigger instances and tweens created inside the component are purged from memory when navigating away, preventing browser lag on subsequent page visits.

Critical Performance Techniques for WebGL Canvases

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:

  • Cap the Device Pixel Ratio: Never render at native 3x or 4x pixel ratios. Use dpr={[1, 2]} in your Canvas component to prevent the GPU from filling millions of unneeded pixels.
  • Use Compressed Assets: Convert GLTF files to GLB format and compress them using KTX2 textures and Draco geometry compression to cut download payloads from 25MB down to 2MB.
  • Bake Static Lighting: Calculate ambient occlusion and detailed shadows in Blender before export. Real-time dynamic shadows consume heavy frame budgets on mobile devices.
  • Implement Viewport Observers: Use IntersectionObserver to stop the requestAnimationFrame render loop entirely when the 3D canvas is scrolled off-screen.

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.

Need Help Implementing This in Your Product?

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.

Frequently Asked Questions

How do you prevent GSAP ScrollTrigger from running twice in React Strict Mode?

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.

Should you store scroll progress in React state to update Three.js models?

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.

How do you maintain 60 FPS on mobile devices with 3D scroll animations?

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.

← Back to all articles