Fix: React Three Fiber Not Working — Canvas Blank, Models Not Loading, or Performance Dropping
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 visibleOr a GLTF model fails to load:
Error: Could not load /model.glb: Unexpected token < in JSON at position 0Or the scene runs at 10 FPS:
Smooth on desktop, slideshow on mobileWhy 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 visible —
meshStandardMaterialandmeshPhysicalMaterialare physically-based materials that require lights in the scene. Without a light source, everything renders black. OnlymeshBasicMaterialignores lighting. - The Canvas needs explicit dimensions —
Canvasfills its parent container. If the parent has zero height (common withdivin flexbox), the canvas is invisible. Give the parent a defined height. - GLTF files must be served as static assets — placing a
.glbfile in the wrong directory (e.g.,src/instead ofpublic/) 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Mapbox GL JS Not Working — Map Not Rendering, Markers Missing, or Access Token Invalid
How to fix Mapbox GL JS issues — access token setup, React integration with react-map-gl, markers and popups, custom layers, geocoding, directions, and Next.js configuration.
Fix: React PDF Not Working — PDF Not Rendering, Worker Error, or Pages Blank
How to fix react-pdf and @react-pdf/renderer issues — PDF viewer setup, worker configuration, page rendering, text selection, annotations, and generating PDFs in React.
Fix: Auth.js (NextAuth) Not Working — Session Null, OAuth Callback Error, or CSRF Token Mismatch
How to fix Auth.js and NextAuth.js issues — OAuth provider setup, session handling in App Router and Pages Router, JWT vs database sessions, middleware protection, and credential provider configuration.