Navigate

Search topics across all sections

GitHub
Production

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.

terminal
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

SuspenseSetup.tsxTSX
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

CustomLoader.tsxTSX
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

Preloading.tsxTSX
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

DreiLoader.tsxTSX
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.

1
Putting Suspense inside the Canvas
useProgress only works outside the Canvas component
Don't do this
SuspensePlacement.tsxTSX
<Canvas>
  {/* useProgress cannot read loading state here */}
  <Suspense fallback={<LoadingScreen />}>
    <Model />
  </Suspense>
</Canvas>
React Suspense works at the React tree level, not the Three.js level. The fallback must be a React component that renders HTML, not a Three.js object. The drei Loader and useProgress hook read the Three.js loading manager state from outside the Canvas.
2
Not preloading assets for instant scene switches
Users see a loading flash every time they navigate
Don't do this
Preloading.tsxTSX
function ModelViewer({ modelPath }) {
  // Loads fresh every time this component mounts
  const { scene } = useGLTF(modelPath)
  return <primitive object={scene} />
}
useGLTF.preload and useTexture.preload start fetching assets immediately when the module loads, before the component even mounts. When the user navigates to that scene, the asset is already cached and appears instantly. Put preload calls at the module level of pages that will need those assets.
3
Showing a blank screen during loading
Users think the page is broken and leave
Don't do this
LoadingUX.tsxTSX
// No fallback — user sees nothing while loading
<Canvas>
  <Model />  {/* Takes 3 seconds to load */}
</Canvas>
Users will wait for content if they can see progress. A loading bar with percentage gives them confidence the page is working. Without it, a 3-second blank screen feels like the page is broken. The useProgress hook from drei gives you loaded count, total count, and percentage.

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.