← Articles
Next.jsThree.jsWebGL

Getting Started with Three.js in Next.js

A step-by-step guide to adding real-time 3D scenes to a Next.js App Router project — including the lazy-loading and cleanup patterns that keep it fast.

August 11, 2026 · 8 min read

Why Three.js and Next.js pair well

Next.js gives you a component model, static rendering, and a built-in optimization pipeline; Three.js gives you a full WebGL renderer without writing GLSL by hand. Used carefully, the combination lets you ship genuinely interactive scenes while keeping the rest of the page as fast as a static site.

The tension between them is real, though. Three.js is a browser API — it touches `window`, `document`, and the GPU. Next.js, by default, renders React on the server, where none of those exist. The fix isn't to avoid Three.js; it's to isolate it behind a client-only boundary and load it lazily.

Install the dependencies

Three.js ships its own TypeScript types, so a single install is all you need:

npm install three
npm install -D @types/three

Build the scene as a component

Keep your scene inside one component with a single `useEffect`. Initialize the renderer, camera, and objects there, run the animation loop, and — this is the part people skip — tear everything down when the component unmounts. An orphaned renderer keeps running forever and leaks memory.

"use client";

import { useEffect, useRef } from "react";
import * as THREE from "three";

export default function ThreeScene() {
  const mountRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const container = mountRef.current;
    if (!container) return;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      45, container.clientWidth / container.clientHeight, 0.1, 1000
    );
    camera.position.z = 8;

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    container.appendChild(renderer.domElement);

    const geometry = new THREE.BoxGeometry();
    const material = new THREE.MeshStandardMaterial({ color: 0x22d3ee });
    const mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);

    let frameId: number;
    const animate = () => {
      mesh.rotation.x += 0.01;
      mesh.rotation.y += 0.01;
      renderer.render(scene, camera);
      frameId = requestAnimationFrame(animate);
    };
    animate();

    return () => {
      cancelAnimationFrame(frameId);
      renderer.dispose();
      geometry.dispose();
      material.dispose();
      if (container.contains(renderer.domElement)) {
        container.removeChild(renderer.domElement);
      }
    };
  }, []);

  return <div ref={mountRef} className="h-64 w-full" />;
}

The critical step: lazy-load it

If you import this component normally, the browser has to download and parse the entire Three.js library before the page can render — even if the scene sits below the fold. The fix is a dynamic import with server-side rendering disabled:

`ssr: false` tells Next.js to skip this component during server rendering (where Three.js would crash anyway) and to load it on the client in a separate bundle. The `loading` prop gives you a placeholder — a spinner, a static gradient — so the layout doesn't jump while the scene hydrates.

import dynamic from "next/dynamic";

const ThreeScene = dynamic(() => import("@/components/three-scene"), {
  ssr: false,
  loading: () => <ScenePlaceholder />,
});

Respect reduced motion

Continuous animation is exactly what `prefers-reduced-motion` users asked their OS to opt out of. It costs almost nothing to check:

The same check works for mouse-tracking listeners: if you're not running a loop, don't attach them.

if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
  renderer.render(scene, camera); // single static frame
} else {
  animate(); // full loop
}

What to do next

Once the pattern above is solid — component boundary, dynamic import, cleanup, reduced motion — you have the foundation for anything: particle fields, product configurators, data visualisation.

  • Add lighting, materials, and post-processing one at a time; measure after each step.
  • Cap the pixel ratio at 2 (or lower on mobile) — the single highest-impact performance fix.
  • Use the React DevTools profiler and the browser Performance panel to watch for layout thrash around your canvas.
  • Read up on draw calls and geometry detail before your scene gets complex — the habits you build now are the ones you'll keep.