Develop

ECS Architecture in WebXR

Updated: Jul 22, 2026
Deep dive into how IWSDK ECS architecture enables scalable, performant WebXR applications.

Data-Oriented Design

ECS follows data-oriented design principles that are especially important for VR performance:

Memory layout: Why columnar storage matters

Columnar storage groups each component field across entities, which keeps related values together.
Traditional OOP stores objects like this:
PlayerObject { health: 100, position: [0,1,0], velocity: [1,0,0] }
EnemyObject  { health: 50,  position: [5,1,2], velocity: [-1,0,0] }
ECS stores the same data like this:
Health:    [100, 50, 75, ...]  // all health values together
Position:  [0,1,0, 5,1,2, ...]  // all positions together
Velocity:  [1,0,0, -1,0,0, ...] // all velocities together
Why this matters in VR:
  • Cache efficiency: When updating health, we only touch health data (no position/velocity).
  • SIMD potential: Process multiple health values in parallel.
  • Memory predictability: Keeps data access regular during frame updates.

Query performance model

Queries compare component masks instead of scanning object fields.
Queries use bit masks for O(1) component checks:
Entity 12: Health(✓) + Position(✓) + AI(✓)     = bitmask: 00000111
Entity 37: Health(✓) + Position(✓) + Player(✓) = bitmask: 00001011

Query { required: [Health, Position] }          = mask:    00000011
  └─ Entity 12: (00000111 & 00000011) == 00000011 ✓ matches
  └─ Entity 37: (00001011 & 00000011) == 00000011 ✓ matches
This makes “find all entities with Health and Position” fast even with thousands of entities.

WebXR Frame Budget

VR applications must hit 72-90fps consistently. IWSDK ECS helps by:

System priority architecture

System priority defines update order.
Frame Budget (11ms for 90fps):
┌─ Input System (-4)          │ 1ms  │ Read controllers, hands
├─ Locomotion (-5)            │ 2ms  │ Update player movement
├─ Physics (-2)               │ 3ms  │ Collision detection
├─ Game Logic (0)             │ 2ms  │ Your gameplay systems
├─ UI System (1)              │ 1ms  │ Update spatial panels
└─ Render prep (2)            │ 2ms  │ Frustum culling, LOD
                              └────┘
                               11ms total budget
Systems with more negative priority run first, ensuring input lag stays minimal.

Query-driven optimization

Queries limit each system to matching entities.
Smart systems only process entities that need updates:
export class LODSystem extends createSystem({
  // Only process visible objects that moved
  needsLODUpdate: {
    required: [Mesh, Transform, Visibility],
    where: [eq(Visibility, 'changed', true)],
  },
}) {
  update() {
    // Process only moved, visible objects
    for (const entity of this.queries.needsLODUpdate.entities) {
      // Update level-of-detail based on distance to camera
      this.updateLOD(entity);
      entity.setValue(Visibility, 'changed', false);
    }
  }
}

Composition Patterns for WebXR

Feature composition

Small components let systems compose behavior.
Build complex VR interactions through component composition:
// Make any object grabbable
entity.addComponent(Grabbable);
entity.addComponent(RigidBody); // Physics integration

// Make it also glowable
entity.addComponent(Interactable, { glowColor: [0, 1, 0] });

// Make it respond to voice commands
entity.addComponent(VoiceTarget, { keywords: ['pick up', 'grab'] });
Each system operates on its own. GrabSystem, GlowSystem, and VoiceSystem can work together without direct links.

Hierarchical entities

Parent-child entities model nested objects.
WebXR often needs nested objects (hand → fingers → joints):
// Create hand hierarchy
const hand = world.createTransformEntity();
hand.addComponent(HandTracking);

const thumb = world.createTransformEntity(undefined, { parent: hand });
thumb.addComponent(FingerJoint, { type: 'thumb' });

const index = world.createTransformEntity(undefined, { parent: hand });
index.addComponent(FingerJoint, { type: 'index' });
Transform system automatically handles parent-child matrix updates.

Scale and Performance Characteristics

Entity limits

Entity capacity depends on active components, query membership, system work, and the rest of the scene. Profile a representative scene on the target headset instead of relying on a fixed entity limit.

Query optimization

Use a defined query instead of checking every entity each frame.
// ❌ Inefficient: checks every entity every frame
for (const entity of world.entities) {
  if (entity.hasComponent(Health) && entity.hasComponent(AI)) {
    // process
  }
}

// ✅ Efficient: precomputed set, direct iteration
for (const entity of this.queries.healthyAI.entities) {
  // process only matching entities
}

Memory usage patterns

Packed component storage and query indices consume more memory as entity and query counts grow. Keep component schemas focused, limit unnecessary query membership, and measure memory and frame time on the target headset.

Integration with Three.js and WebXR

Object3D synchronization

IWSDK links ECS entities to Three.js objects.
IWSDK bridges ECS data and Three.js scene graph:
ECS Side:              Three.js Side:
─────────              ──────────────
Entity 12 ←──linked──→ Object3D
├─ Transform           ├─ position: [1,2,3]
│  └─ position: [1,2,3] └─ quaternion: [...]
└─ Mesh                └─ mesh: BoxGeometry
   └─ geometry: "box"
Transform system automatically syncs ECS component data to Three.js matrices.

WebXR session lifecycle

The world exposes WebXR session state to systems.
Session Start:
├─ World.visibilityState → 'visible'
├─ Input systems activate (hand/controller tracking)
├─ Locomotion systems start physics updates
└─ Render loop switches to XR frame timing (90fps)

Session End:
├─ World.visibilityState → 'non-immersive'
├─ Input systems pause expensive tracking
├─ `world.camera` auto-restored to its pre-XR local transform/projection
│  (one rAF after WebXRManager teardown; opt out with
│  `xr: { restoreCameraOnExit: false }`)
└─ Render loop returns to 60fps
Systems can check world.visibilityState to reduce CPU load when VR headset is removed.

Debugging and Profiling

Query analysis

Query counts show how much work a system can perform.
console.log('System performance:');
for (const [name, query] of Object.entries(this.queries)) {
  console.log(`  ${name}: ${query.entities.size} entities`);
}

Component distribution

Inspect component membership when profiling entity composition.
// Check component distribution
world.entityManager.entities.forEach((entity) => {
  console.log(
    `Entity ${entity.index}:`,
    entity.getComponents().map((c) => c.id),
  );
});

System timing

Enable built-in performance monitoring to inspect per-system frame time:
world.enablePerformanceMonitoring = true; // Shows system timing
This design supports both small demos and larger VR apps. Profile the app to keep frame time stable as the scene grows.
See ECS concepts for related entity, component, query, and system guidance.