Navigate
Search topics across all sections
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?
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
<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
<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
<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
// 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.
// 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>
))}// Raycasting a 100K-triangle model checks ALL triangles
<primitive
object={scene}
onClick={(e) => console.log(e.point)}
/>// Downloads happen sequentially as each component mounts
<Suspense fallback={<Loader />}>
<Character /> {/* Downloads character.glb */}
<Terrain /> {/* Waits, then downloads textures */}
</Suspense>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.