VR Performance Optimization
Diagnose and fix performance issues in VR experiences. Covers profiling, common bottlenecks, and Quest-specific tips.
VR has stricter performance requirements than desktop or mobile web. This guide helps you identify and fix common performance bottlenecks in VR projects.
:::tip New to WebXR with Needle Engine? Start with the WebXR Overview to learn about supported platforms, setup, and features before diving into optimization. :::
Why VR Performance is Different
VR headsets render your scene twice per frame (once per eye). Combined with high refresh rates, the performance budget is tight:
Meta Quest 2
72β90 Hz
11β14 ms
Meta Quest 3
72β120 Hz
8β14 ms
Apple Vision Pro
90 Hz
11 ms
PC VR (Index, Vive)
90β144 Hz
7β11 ms
If your frame takes longer than the budget, the headset will drop frames, causing judder β which is uncomfortable and can cause motion sickness.
Quest headsets use mobile GPUs, comparable to a mid-range phone. They can't handle the same scene complexity as a desktop GPU.
How to Profile VR Performance
1. Use the stats overlay
Append ?stats to your URL to show an FPS counter and renderer statistics (draw calls, triangles, textures in memory). This works while in VR.
2. Connect Chrome DevTools to Quest
Quest runs a Chromium-based browser. You can profile it from your PC:
Connect Quest to your PC via USB
Open
chrome://inspect/#devicesin ChromeFind your page and click Inspect
Use the Performance tab to record a trace while in VR
See Quest Debugging for more details.
3. Identify CPU vs GPU bottleneck
CPU bound: long JavaScript execution in the Performance flame chart (physics, scripts, draw call submission). The GPU finishes early but the CPU can't keep up.
GPU bound: short JavaScript frames but the headset still drops frames. The scene is too complex for the GPU to render in time.
Most VR performance issues on Quest are GPU bound due to the mobile hardware.
Common Bottlenecks & Fixes
Draw calls
Each unique mesh + material combination is a separate draw call. The browser must communicate each one to the GPU, which adds overhead.
Target: Keep draw calls under ~100β150 for Quest. Desktop VR can handle more.
How to reduce them:
What to do
Why it helps
Mark objects as static
Needle Engine disables matrix updates at runtime, saving CPU time
Merge static meshes before export
Fewer objects = fewer draw calls
Reduce unique materials
Objects sharing the same material are cheaper to render
Enable GPU instancing on materials
Needle Engine batches instanced objects automatically, no code needed
Check with ?stats URL parameter
Shows your current draw call count
Shadows
Realtime shadows are one of the most expensive rendering features. Each shadow-casting light requires rendering shadow maps, which in VR means additional render passes.
Fixes:
What to do
Why it helps
Remove realtime shadows as a first test
If performance improves dramatically, shadows are the bottleneck
Use baked lightmaps
Pre-computed lighting for static scenes β zero runtime cost
Use ContactShadows
Lightweight ground shadows without full shadow maps
Limit to a single directional light
Each shadow-casting light adds extra render passes
Reduce shadow map resolution and distance
Less GPU work for shadows β in Unity, add the Additional Light Data component to your light (button at the bottom of the Light component) to control resolution per light
Transparent materials and overdraw
Transparent objects (glass, particles, UI panels) can't use the GPU's depth buffer to skip hidden pixels. Each transparent layer is drawn fully, and they must be sorted back-to-front.
Fixes:
What to do
Why it helps
Minimize transparent objects in the scene
Each transparent layer is fully drawn and can't be skipped
Reduce screen coverage of transparent objects
Smaller particle effects, smaller UI panels = less overdraw
Use alpha cutout (alphaTest) or alpha hash
Both work with the depth buffer, unlike alpha blending. Alpha hash is recommended β it avoids hard cutoff edges while keeping depth buffer benefits
Texture memory
Large textures consume GPU memory and bandwidth. On Quest, GPU memory is shared with system RAM.
:::tip Production builds already follow best practices Needle Engine production builds automatically apply texture compression (KTX2), progressive loading, and mesh LODs. If you're testing locally and seeing poor texture performance, enable Preview Compression on the Needle Engine component to run the full production pipeline during development. See Build Options for details. :::
Fixes:
What to do
Why it helps
Use texture compression (KTX2)
Compressed textures use 4β8x less GPU memory than uncompressed
Reduce texture resolution where possible
Floor textures viewed at an angle don't need 4K
Enable progressive texture loading
Low-res loads first, full quality on demand, unused textures released from GPU memory
Avoid WebP textures for VR
WebP is uncompressed in GPU memory despite small file size
Physics
Physics simulation (Rapier) runs on the CPU and can become expensive with many active rigidbodies or complex collider shapes.
Fixes:
What to do
Why it helps
Use simple collider shapes (box, sphere, capsule)
Mesh colliders are much more expensive to compute
Reduce active rigidbodies
Each active body adds to the physics simulation cost
Set objects to kinematic or static
Only dynamic bodies need full simulation each frame
Disable colliders on non-interactive objects
Fewer colliders = less work for the physics engine
Expensive scripts
Scripts that run every frame (update()) add to the CPU cost. In VR, even small per-frame costs add up because the frame budget is tight.
Fixes:
What to do
Why it helps
Profile your code in Chrome DevTools
Find the expensive functions before guessing
Avoid allocations in update()
Creating new objects/arrays triggers garbage collection frame spikes
Use events instead of polling in update()
Skips unnecessary checks when nothing has changed
Cache component lookups and calculations
e.g. calling findObjectOfType every frame is expensive β do it once in start() and store the result
Quest-Specific Tips
Meta Quest headsets have the tightest constraints because they use mobile GPUs. Here are Quest-specific recommendations:
Disable shadows on Quest:
See Detect Mobile Devices for more device detection utilities.
General Quest guidelines:
Target under 100 draw calls
Keep triangle count under 200β300k visible at any time
Use compressed textures (KTX2) β they save both memory and bandwidth
Avoid realtime shadows β use baked lighting or ContactShadows
Quick Reference
π¦ Build
Use a production build (compression, LODs enabled)
π¨ Draw calls
Under ~100 on Quest, check with ?stats
π Shadows
Baked lightmaps or ContactShadows instead of realtime
πΌοΈ Textures
Compressed with KTX2, reasonable resolutions
π Transparency
Minimal, prefer alpha cutout or alpha hash
βοΈ Physics
Simple collider shapes, few active rigidbodies
π Scripts
No heavy work or allocations in update()
π₯½ Testing
Always test on the actual headset
Related Documentation
Optimization & Compression β texture and mesh compression, LODs, progressive loading
Debugging Parameters β URL parameters, Quest debugging setup
Detect Mobile Devices β device detection for adaptive quality
ContactShadows β lightweight ground shadow alternative
WebXR Overview β XR features and platform support
Last updated