docs
Getting Started
Tutorials
How-To Guides
Explanation
Reference
Help
Getting Started
Tutorials
How-To Guides
Explanation
Reference
Help

WebXRVR Performance Optimization

VR has stricter performance requirements than desktop or mobile web. This guide helps you identify and fix common performance bottlenecks in VR projects.

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:

HeadsetRefresh RateFrame Budget
MetaMeta Quest 272–90 Hz11–14 ms
MetaMeta Quest 372–120 Hz8–14 ms
AppleApple Vision Pro90 Hz11 ms
PC VR (Index, Vive)90–144 Hz7–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:

  1. Connect Quest to your PC via USB
  2. Open chrome://inspect/#devices in Chrome
  3. Find your page and click Inspect
  4. 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:

build What to dolightbulb Why it helps
Mark objects as staticNeedle Engine disables matrix updates at runtime, saving CPU time
Merge static meshes before exportFewer objects = fewer draw calls
Reduce unique materialsObjects sharing the same material are cheaper to render
Enable GPU instancing on materialsNeedle Engine batches instanced objects automatically, no code needed
Check with ?stats URL parameterShows 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:

build What to dolightbulb Why it helps
Remove realtime shadows as a first testIf performance improves dramatically, shadows are the bottleneck
Use baked lightmapsPre-computed lighting for static scenes — zero runtime cost
Use ContactShadowsLightweight ground shadows without full shadow maps
Limit to a single directional lightEach shadow-casting light adds extra render passes
Reduce shadow map resolution and distanceLess 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:

build What to dolightbulb Why it helps
Minimize transparent objects in the sceneEach transparent layer is fully drawn and can't be skipped
Reduce screen coverage of transparent objectsSmaller particle effects, smaller UI panels = less overdraw
Use alpha cutout (alphaTest) or alpha hashBoth 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.

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:

build What to dolightbulb Why it helps
Use texture compression (KTX2)Compressed textures use 4–8x less GPU memory than uncompressed
Reduce texture resolution where possibleFloor textures viewed at an angle don't need 4K
Enable progressive texture loadingLow-res loads first, full quality on demand, unused textures released from GPU memory
Avoid WebP textures for VRWebP 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:

build What to dolightbulb Why it helps
Use simple collider shapes (box, sphere, capsule)Mesh colliders are much more expensive to compute
Reduce active rigidbodiesEach active body adds to the physics simulation cost
Set objects to kinematic or staticOnly dynamic bodies need full simulation each frame
Disable colliders on non-interactive objectsFewer 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:

build What to dolightbulb Why it helps
Profile your code in Chrome DevToolsFind 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 calculationse.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:

import { Behaviour, DeviceUtilities } from "@needle-tools/engine";

export class VRQualitySettings extends Behaviour {
    start() {
        if (DeviceUtilities.isQuest()) {
            this.context.renderer.shadowMap.enabled = false;
        }
    }
}

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

AreaWhat to check
📦 BuildUse a production build (compression, LODs enabled)
🎨 Draw callsUnder ~100 on Quest, check with ?stats
🌑 ShadowsBaked lightmaps or ContactShadows instead of realtime
🖼️ TexturesCompressed with KTX2, reasonable resolutions
💎 TransparencyMinimal, prefer alpha cutout or alpha hash
⚙️ PhysicsSimple collider shapes, few active rigidbodies
📝 ScriptsNo heavy work or allocations in update()
🥽 TestingAlways 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
Suggest changes
Last Updated: 2/27/26, 11:07 AM

Extras

ChatGPT Ask ChatGPT Claude Ask Claude
Copy Markdown

Navigation

  • Getting Started
  • Tutorials
  • How-To Guides
  • Explanation
  • Reference
  • Help

Extras

ChatGPT Ask ChatGPT Claude Ask Claude
Copy Markdown