Navigate

Search topics across all sections

GitHub
Performance

Drei Performance Helpers

You know the optimization theory -- reduce draw calls, avoid re-renders, measure everything. But writing low-level instancing code and spatial trees from scratch is tedious. Drei gives you ready-made performance tools that solve the most common bottlenecks with just a few components.

You load a detailed car model (100K triangles) and add onClick and onPointerOver handlers. Every time you move the mouse, the frame rate drops to 15 FPS. The scene is not even that complex -- what is eating all the performance?

terminal
Performance: Frame time 65ms (15 FPS) | Raycast: testing 102,400 triangles per pointer event

Real-world

Think of drei performance helpers like kitchen prep in a restaurant.

A great chef does not start cooking when the customer orders. They prep ingredients ahead of time (preload assets). They use one big pan for batch frying(Instances). They only cook what customers ordered(demand rendering). And they organize the kitchen so any ingredient is found instantly (BVH for fast raycasting).

Preload

Download assets early

Instance

Batch similar meshes

BVH

Speed up raycasting

Adapt

Lower quality under load

The Drei Performance Toolkit

Drei provides four categories of performance helpers. Each one tackles a different bottleneck. Let's walk through them.

Instances -- one draw call for hundreds of objects

InstancesComponent.tsxTSX
<Instances limit={1000} range={1000}>
  <boxGeometry args={[0.2, 1, 0.2]} />
  <meshStandardMaterial />
  {items.map((_, i) => (
    <Instance key={i} position={pos[i]} color={col[i]} />
  ))}
</Instances>

Each Instance is a regular React component with position, rotation, scale, and color props. Behind the scenes, they all render in a single draw call. Use this whenever you have 10+ copies of the same geometry.

Bvh -- fast raycasting for complex models

BvhAcceleration.tsxTSX
<Bvh firstHitOnly>
  <mesh onClick={(e) => console.log(e.point)}>
    <torusKnotGeometry args={[1, 0.3, 256, 64]} />
    <meshStandardMaterial />
  </mesh>
</Bvh>

Without BVH, every pointer event tests the ray against every triangle. With it, the test drops from O(n) to O(log n). A 100K triangle model goes from 100,000 tests to about 20 bounding box checks. Use firstHitOnly when you only need the nearest hit.

Adaptive Performance -- auto-degrade under load

AdaptivePerformance.tsxTSX
<Canvas dpr={[1, 2]}>
  <AdaptiveDpr pixelated />
  <AdaptiveEvents />
  <PerformanceMonitor
    onDecline={() => setQuality('low')}
    flipflops={3}
  />
</Canvas>

AdaptiveDpr lowers the pixel ratio when the GPU struggles. AdaptiveEvents reduces pointer event frequency. And PerformanceMonitor lets you trigger custom quality changes (lowering shadow resolution, reducing particle count) when performance dips.

Preloading + Demand Rendering -- load early, render lazy

PreloadAndDemand.tsxTSX
// Start downloads immediately at module level
useGLTF.preload('/models/character.glb')
useTexture.preload('/textures/ground.jpg')

// Only render when something changes
<Canvas frameloop="demand">
  <OrbitControls />  {/* Auto-invalidates on move */}
</Canvas>

Preload starts all downloads in parallel before components mount, eliminating the loading waterfall. Demand rendering only draws a new frame when the scene actually changes, saving GPU and battery on static or rarely-updated scenes.

What you just learned

Instances batches hundreds of identical meshes into a single draw call with a declarative React API.

Bvh builds a spatial tree that makes raycasting nearly instant on complex geometry (O(log n) instead of O(n)).

AdaptiveDpr and PerformanceMonitor automatically lower quality when the GPU struggles.

Preloading assets at module level parallelizes downloads and eliminates the loading waterfall.

Demand rendering with frameloop='demand' only renders when something changes, saving GPU and battery.

Question

If BVH makes raycasting so much faster, why is it not enabled by default on every mesh in Three.js? What is the tradeoff?

Think about it...

You have a 3D product viewer with a detailed model (50K triangles) and OrbitControls. The user can hover over parts to highlight them. The scene is mostly static -- the camera only moves when the user drags. Which drei helpers would give you the biggest wins?

Hint: What are the two main performance problems? Hover is slow (raycasting bottleneck), and the scene renders 60 FPS even when nothing moves (wasted GPU work).

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

Set count to 1000 — still smooth!

Try This!

Beginner

Max waveHeight — dramatic waves

Try This!

Beginner

Set cubeSize to 0.05 — pixel art

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Using individual meshes when Instances would work
Hundreds of separate meshes create hundreds of draw calls
Don't do this
InstancesVsMeshes.tsxTSX
// 500 meshes = 500 draw calls
{Array.from({ length: 500 }).map((_, i) => (
  <mesh key={i} position={[Math.random() * 20, 0, 0]}>
    <sphereGeometry args={[0.1, 8, 8]} />
    <meshStandardMaterial />
  </mesh>
))}
Each individual mesh creates a separate draw call -- a CPU-GPU round trip. Drei's Instances component batches all child Instance components into a single InstancedMesh, reducing hundreds of draw calls to one. Use Instances whenever you have many copies of the same geometry.
2
Not using BVH for interactive complex geometry
Raycasting without BVH tests every triangle individually
Don't do this
BvhRaycast.tsxTSX
// Raycasting a 100K-triangle model checks ALL triangles
<primitive
  object={scene}
  onClick={(e) => console.log(e.point)}
/>
Without BVH, every pointer event (hover, click) tests the ray against every triangle in the geometry. For a detailed model, that is 100,000+ tests per frame while the mouse moves. The Bvh component builds a spatial tree that skips entire regions, reducing tests from thousands to about 20-30 bounding box checks.
3
Not preloading assets before components mount
Assets download one at a time as components render
Don't do this
PreloadAssets.tsxTSX
// Downloads happen sequentially as each component mounts
<Suspense fallback={<Loader />}>
  <Character />   {/* Downloads character.glb */}
  <Terrain />     {/* Waits, then downloads textures */}
</Suspense>
Without preloading, each component downloads its resources only when it first mounts. In a Suspense tree, this creates a waterfall: one finishes, the next starts. Calling preload() at the module level starts all downloads in parallel as soon as the JavaScript file loads, long before components render.

Best Practices

Instance Similar Objects

Whenever you have 10+ copies of the same geometry, use Instances. Same declarative React API, but everything renders in a single draw call.

BVH for Any Interactive Model

Wrap any clickable or hoverable scene with Bvh, especially models over 10K triangles. Without it, pointer events on complex geometry will tank your frame rate.

Preload Everything

Call useGLTF.preload() and useTexture.preload() at module level for all assets. This parallelizes downloads and eliminates Suspense loading waterfalls.

Use Detailed for LOD

For large outdoor scenes, use drei's Detailed component to swap models based on camera distance. Reduces triangle count by 80%+ for distant objects.