Skip to content

Fix: React Three Fiber Not Working — Canvas Blank, Models Not Loading, or Performance Dropping

FixDevs ·

Quick Answer

How to fix React Three Fiber (R3F) issues — Canvas setup, loading 3D models with useGLTF, lighting, camera controls, animations with useFrame, post-processing, and Next.js integration.

The Problem

The Canvas component renders a black or empty box:

import { Canvas } from '@react-three/fiber';

function Scene() {
  return (
    <Canvas>
      <mesh>
        <boxGeometry />
        <meshStandardMaterial color="red" />
      </mesh>
    </Canvas>
  );
}
// Black square — nothing visible

Or a GLTF model fails to load:

Error: Could not load /model.glb: Unexpected token < in JSON at position 0

Or the scene runs at 10 FPS:

Smooth on desktop, slideshow on mobile

Why This Happens

React Three Fiber (R3F) is a React renderer for Three.js. It maps Three.js objects to JSX components, but the underlying 3D rendering concepts still apply:

  • Meshes need lights to be visiblemeshStandardMaterial and meshPhysicalMaterial are physically-based materials that require lights in the scene. Without a light source, everything renders black. Only meshBasicMaterial ignores lighting.
  • The Canvas needs explicit dimensionsCanvas fills its parent container. If the parent has zero height (common with div in flexbox), the canvas is invisible. Give the parent a defined height.
  • GLTF files must be served as static assets — placing a .glb file in the wrong directory (e.g., src/ instead of public/) causes the server to return an HTML 404 page, which Three.js tries to parse as JSON.
  • Three.js is CPU/GPU intensive — complex scenes with many meshes, high-resolution textures, or heavy post-processing overwhelm mobile GPUs. Performance optimization (instancing, LOD, frustum culling) is essential.

Fix 1: Basic Scene with Lighting

npm install @react-three/fiber @react-three/drei three
npm install -D @types/three
'use client';

import { Canvas } from '@react-three/fiber';
import { OrbitControls, Environment } from '@react-three/drei';

function Scene() {
  return (
    // Parent must have a defined height
    <div style={{ width: '100%', height: '100vh' }}>
      <Canvas
        camera={{ position: [3, 3, 3], fov: 50 }}
        shadows
      >
        {/* Lighting — essential for PBR materials */}
        <ambientLight intensity={0.5} />
        <directionalLight
          position={[5, 5, 5]}
          intensity={1}
          castShadow
          shadow-mapSize={[1024, 1024]}
        />

        {/* Or use an environment map for realistic lighting */}
        {/* <Environment preset="sunset" /> */}

        {/* Objects */}
        <mesh castShadow position={[0, 0.5, 0]}>
          <boxGeometry args={[1, 1, 1]} />
          <meshStandardMaterial color="#4299e1" roughness={0.3} metalness={0.1} />
        </mesh>

        {/* Floor */}
        <mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>
          <planeGeometry args={[10, 10]} />
          <meshStandardMaterial color="#e2e8f0" />
        </mesh>

        {/* Camera controls — drag to rotate, scroll to zoom */}
        <OrbitControls
          enableDamping
          dampingFactor={0.05}
          minDistance={2}
          maxDistance={20}
        />
      </Canvas>
    </div>
  );
}

Fix 2: Load 3D Models (GLTF/GLB)

# Place model files in the public/ directory
# public/models/robot.glb
'use client';

import { Canvas } from '@react-three/fiber';
import { useGLTF, OrbitControls, Stage } from '@react-three/drei';
import { Suspense } from 'react';

// Preload the model
useGLTF.preload('/models/robot.glb');

function Robot(props: JSX.IntrinsicElements['group']) {
  const { scene } = useGLTF('/models/robot.glb');

  return <primitive object={scene} {...props} />;
}

// With typed nodes (after running gltfjsx)
// npx gltfjsx public/models/robot.glb --types --transform
function RobotDetailed(props: JSX.IntrinsicElements['group']) {
  const { nodes, materials } = useGLTF('/models/robot.glb');

  return (
    <group {...props} dispose={null}>
      <mesh
        geometry={nodes.Body.geometry}
        material={materials.Metal}
        castShadow
      />
      <mesh
        geometry={nodes.Head.geometry}
        material={materials.Metal}
        position={[0, 1.5, 0]}
        castShadow
      />
    </group>
  );
}

function ModelViewer() {
  return (
    <div style={{ width: '100%', height: '80vh' }}>
      <Canvas shadows camera={{ position: [0, 2, 5], fov: 45 }}>
        {/* Suspense for async model loading */}
        <Suspense fallback={null}>
          {/* Stage provides automatic lighting and centering */}
          <Stage environment="city" intensity={0.5}>
            <Robot scale={1} />
          </Stage>
        </Suspense>
        <OrbitControls autoRotate autoRotateSpeed={1} />
      </Canvas>
    </div>
  );
}

Fix 3: Animations with useFrame

'use client';

import { Canvas, useFrame } from '@react-three/fiber';
import { useRef } from 'react';
import * as THREE from 'three';

function RotatingCube() {
  const meshRef = useRef<THREE.Mesh>(null);

  // useFrame runs every frame (60fps)
  useFrame((state, delta) => {
    if (!meshRef.current) return;
    meshRef.current.rotation.x += delta * 0.5;
    meshRef.current.rotation.y += delta * 0.3;
  });

  return (
    <mesh ref={meshRef}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color="hotpink" />
    </mesh>
  );
}

// Animated with hover/click state
function InteractiveBox() {
  const meshRef = useRef<THREE.Mesh>(null);
  const [hovered, setHovered] = useState(false);
  const [clicked, setClicked] = useState(false);

  useFrame((state, delta) => {
    if (!meshRef.current) return;
    // Smooth scale animation
    const target = clicked ? 1.5 : 1;
    meshRef.current.scale.lerp(new THREE.Vector3(target, target, target), delta * 5);
  });

  return (
    <mesh
      ref={meshRef}
      onClick={() => setClicked(!clicked)}
      onPointerOver={() => setHovered(true)}
      onPointerOut={() => setHovered(false)}
    >
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  );
}

// Animated GLTF model with mixer
import { useAnimations, useGLTF } from '@react-three/drei';

function AnimatedCharacter() {
  const group = useRef<THREE.Group>(null);
  const { scene, animations } = useGLTF('/models/character.glb');
  const { actions } = useAnimations(animations, group);

  useEffect(() => {
    // Play the "walk" animation
    actions['walk']?.reset().fadeIn(0.5).play();
    return () => { actions['walk']?.fadeOut(0.5); };
  }, [actions]);

  return <primitive ref={group} object={scene} />;
}

Fix 4: Text, HTML Overlays, and UI

import { Html, Text, Text3D, Float, Billboard } from '@react-three/drei';

// 3D text (using troika-three-text under the hood)
function TextExample() {
  return (
    <Text
      position={[0, 2, 0]}
      fontSize={0.5}
      color="white"
      anchorX="center"
      anchorY="middle"
      font="/fonts/Inter-Bold.woff"
    >
      Hello World
    </Text>
  );
}

// HTML overlay in 3D space
function HtmlOverlay() {
  return (
    <mesh position={[2, 1, 0]}>
      <sphereGeometry args={[0.3]} />
      <meshStandardMaterial color="red" />
      <Html
        distanceFactor={10}
        position={[0, 0.5, 0]}
        center
        className="pointer-events-auto"
      >
        <div className="bg-white rounded-lg shadow-xl p-3 text-sm w-48">
          <p className="font-bold">Info Point</p>
          <p className="text-gray-600">Click for details</p>
        </div>
      </Html>
    </mesh>
  );
}

// Floating animation
function FloatingLogo() {
  return (
    <Float
      speed={2}
      rotationIntensity={0.5}
      floatIntensity={1}
    >
      <mesh>
        <torusKnotGeometry args={[1, 0.3, 128, 32]} />
        <meshStandardMaterial color="#8b5cf6" metalness={0.8} roughness={0.2} />
      </mesh>
    </Float>
  );
}

Fix 5: Next.js App Router Integration

// R3F uses browser APIs — must be client-only
// components/Scene.tsx
'use client';

import { Canvas } from '@react-three/fiber';
import { OrbitControls, Environment } from '@react-three/drei';
import { Suspense } from 'react';

export function Scene() {
  return (
    <Canvas>
      <Suspense fallback={null}>
        <Environment preset="sunset" />
        <mesh>
          <boxGeometry />
          <meshStandardMaterial color="blue" />
        </mesh>
        <OrbitControls />
      </Suspense>
    </Canvas>
  );
}

// app/page.tsx — Server Component can import client Scene
import { Scene } from '@/components/Scene';

export default function Home() {
  return (
    <main>
      <h1>3D Viewer</h1>
      <div style={{ height: '600px' }}>
        <Scene />
      </div>
    </main>
  );
}

// Dynamic import for code splitting (optional)
import dynamic from 'next/dynamic';
const Scene = dynamic(() => import('@/components/Scene').then(m => m.Scene), {
  ssr: false,
  loading: () => <div style={{ height: '600px', background: '#111' }}>Loading 3D...</div>,
});

Fix 6: Performance Optimization

import { useFrame } from '@react-three/fiber';
import { Instances, Instance, PerformanceMonitor, AdaptiveDpr } from '@react-three/drei';

// Instanced rendering — thousands of objects efficiently
function InstancedBoxes({ count = 1000 }) {
  const data = useMemo(() =>
    Array.from({ length: count }, () => ({
      position: [
        (Math.random() - 0.5) * 20,
        (Math.random() - 0.5) * 20,
        (Math.random() - 0.5) * 20,
      ] as [number, number, number],
      rotation: [Math.random() * Math.PI, Math.random() * Math.PI, 0] as [number, number, number],
      scale: 0.2 + Math.random() * 0.3,
    })), [count]
  );

  return (
    <Instances limit={count}>
      <boxGeometry />
      <meshStandardMaterial color="#4299e1" />
      {data.map((props, i) => (
        <Instance key={i} position={props.position} rotation={props.rotation} scale={props.scale} />
      ))}
    </Instances>
  );
}

// Adaptive performance
function PerformantScene() {
  return (
    <Canvas>
      {/* Auto-adjust DPR based on performance */}
      <AdaptiveDpr pixelated />

      {/* Monitor performance and adjust */}
      <PerformanceMonitor
        onIncline={() => console.log('Performance improving')}
        onDecline={() => console.log('Performance declining')}
        onChange={({ factor }) => {
          // factor: 0 (bad) to 1 (good)
          // Use to adjust quality dynamically
        }}
      />

      {/* Frustum culling is on by default */}
      <mesh frustumCulled>
        <boxGeometry />
        <meshStandardMaterial />
      </mesh>
    </Canvas>
  );
}

Still Not Working?

Canvas is black — add lights. meshStandardMaterial requires at least an ambientLight or directionalLight. For quick results, use <Environment preset="sunset" /> from @react-three/drei, which provides image-based lighting.

Model shows “Unexpected token <” — the model file isn’t served correctly. Place .glb files in public/models/ and reference them as /models/robot.glb (without public/). If the dev server returns HTML instead of the binary file, the path is wrong.

Scene freezes or crashes on mobile — reduce complexity. Disable shadows (shadows={false} on Canvas), lower texture resolution, reduce polygon count, and use <AdaptiveDpr /> to lower the pixel ratio. For many objects, use <Instances> instead of individual <mesh> elements.

useFrame causes “hooks can only be called inside Canvas” — components using R3F hooks (useFrame, useThree, useGLTF) must be children of <Canvas>, not siblings. The Canvas creates a separate React reconciler — R3F hooks only work inside it.

For related animation issues, see Fix: Framer Motion Not Working and Fix: Next.js App Router Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles