Navigate
Search topics across all sections
Loading Progress
Your 3D scene takes four seconds to load. During that time, users stare at a blank screen and wonder if the page is broken. A proper loading experience turns that dead time into anticipation.
You deploy a scene with a 5MB GLTF model and several 2K textures. On a fast connection it takes 3 seconds. On mobile 4G it takes 12 seconds. Users see a white page the entire time, assume it is broken, and bounce.
UX: No loading indicator | Users see blank screen for 12s on 4G | Bounce rate: 73% | No Suspense boundary — React throws unhandled promise
Real-world
Think of loading assets like an airport baggage carousel.
Your textures, models, and HDR maps are checked luggage arriving on the conveyor belt. The progress bar is the arrivals screen showing how many bags have landed. React Suspense is the "please wait here" sign that keeps passengers in the right area. And drei's useProgress hook gives you the exact bag count.
Without these, your passengers are standing in an empty terminal with no information -- they will leave.
Assets Requested
GLTF, textures, HDR
useProgress
Tracks loaded / total
Loading UI
Progress bar + percent
Suspense Resolves
All assets ready
Scene Visible
Smooth reveal
Building a Production Loading Screen
A loading screen in R3F combines two mechanisms: React Suspense for flow control and drei's useProgress for the actual numbers. Here is how to wire them together.
Step 1: Wrap your scene in Suspense
import { Suspense } from 'react'
import { Canvas } from '@react-three/fiber'
function App() {
return (
<Canvas>
<Suspense fallback={null}>
<MyScene />
</Suspense>
</Canvas>
)
}Suspense catches the "loading promise" thrown by hooks like useGLTF and useTexture. The fallback inside Canvas must be null or a Three.js component. Your HTML loading screen lives outside the Canvas.
Step 2: Build a custom loading component with useProgress
import { useProgress } from '@react-three/drei'
function LoadingScreen() {
const { progress, loaded, total, item } = useProgress()
return (
<div className="loading-overlay">
<div className="progress-bar"
style={{ width: `${progress}%` }} />
<p>{loaded} / {total} assets</p>
<p>{Math.round(progress)}%</p>
<p className="current-item">{item}</p>
</div>
)
}useProgress returns four values: progress (0-100), loaded (count of finished assets), total (count of all assets), and item (the URL of the asset currently loading). Use these to build any loading UI you want -- progress bars, spinners, or even a mini 3D animation.
Step 3: Preload assets for instant transitions
import { useGLTF, useTexture } from '@react-three/drei'
// These run at module load time — before any component mounts
useGLTF.preload('/models/car.glb')
useGLTF.preload('/models/bike.glb')
useTexture.preload('/textures/ground.jpg')
// When the component mounts, assets are already cached
function CarScene() {
const { scene } = useGLTF('/models/car.glb')
return <primitive object={scene} />
}Preloading starts fetching assets the moment your JavaScript module is parsed, not when the component mounts. If you know the user will need certain assets (like the next page in a multi-scene app), preload them so there is zero loading delay when they navigate.
Step 4: Use drei's built-in Loader for quick prototyping
import { Loader } from '@react-three/drei'
function App() {
return (
<>
<Canvas>
<Suspense fallback={null}>
<MyScene />
</Suspense>
</Canvas>
{/* Drop-in loading bar — sits outside Canvas */}
<Loader
containerStyles={{ background: '#0a0a0a' }}
barStyles={{ background: '#6366f1' }}
dataStyles={{ color: '#fff' }}
/>
</>
)
}If you do not need a custom design, drei's Loader component is a ready-made progress bar. It uses useProgress internally and renders an HTML overlay with a progress bar, percentage, and current item. Customize it with style props or use it as a starting point.
What you just learned
React Suspense catches loading promises from hooks like useGLTF and useTexture.
useProgress from drei gives you progress (0-100), loaded count, total count, and current item URL.
Loading UI must be HTML outside the Canvas — Suspense fallback inside Canvas must be null or Three.js objects.
useGLTF.preload and useTexture.preload at module level start fetching before components mount.
drei's Loader component is a drop-in progress bar for quick prototyping.
Question
You have a multi-page app where page 2 loads a 10MB GLTF model. Users always visit page 1 first. How would you make the transition to page 2 feel instant, without loading the model on page 1?
Think about it...
You have a scene with 3 GLTF models and 5 textures. useProgress shows progress jumping from 0% to 37% to 100% with nothing in between. Why are you not seeing smooth 0-12.5-25-37.5-... increments?
Hint: How does a browser handle multiple fetch requests? Does it download them one at a time?
Try These Challenges
Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.
Try This!
Beginner
Click Reload — watch the progress bar
Try This!
Beginner
Note the percentage jumping — that's assets loading
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
<Canvas>
{/* useProgress cannot read loading state here */}
<Suspense fallback={<LoadingScreen />}>
<Model />
</Suspense>
</Canvas>function ModelViewer({ modelPath }) {
// Loads fresh every time this component mounts
const { scene } = useGLTF(modelPath)
return <primitive object={scene} />
}// No fallback — user sees nothing while loading
<Canvas>
<Model /> {/* Takes 3 seconds to load */}
</Canvas>Best Practices
Animate the Progress Bar
Use CSS transition on the progress bar width so it glides smoothly instead of jumping. Users perceive smooth movement as faster loading, even when the actual time is the same.
Preload on Route Hover
If your app has navigation, start preloading assets when the user hovers over a link. By the time they click, the assets may already be cached. This gives you instant scene transitions for free.
Compress Your Assets
Use Draco compression for GLTF models (70-90% smaller) and KTX2/Basis for textures (4-6x smaller). Smaller assets mean faster loading, which means shorter loading screens and happier users.
Fade In After Loading
Do not snap from loading screen to scene. Add a short fade transition (300-500ms) when progress hits 100%. This hides any jank from the first render pass and feels polished.