This article outlines a scalable and maintainable code architecture for building WebXR experiences using Three.js. Proper structuring enhances team collaboration, improves performance, simplifies debugging, and allows smooth expansion of features and scenes over time.
Why Code Structuring Matters
Unstructured or monolithic code can quickly become unmanageable, especially in complex XR projects. By modularizing and organizing code components, developers can:
- Reduce cognitive load
- Enable code reuse
- Improve debugging and maintenance
- Separate concerns (e.g., UI logic vs. rendering logic)
- Facilitate asset and scene management
Recommended Folder Structure
A scalable file structure typically looks like this:
bash Copy Edit /src ├── core/ # Renderer, scene, camera, loop, XR ├── components/ # Reusable UI or interaction components ├── systems/ # Game/scene logic, input, animation systems ├── assets/ │ ├── models/ │ ├── textures/ │ └── shaders/ ├── scenes/ # Scene modules (Scene1, Scene2, Lobby, etc.) ├── utils/ # Helpers, math, physics, loaders ├── managers/ # State, event, or UI managers ├── main.js # App entry point └── config.js # Global config and constants
Code Responsibility Breakdown
1. Core Initialization (/core)
Handles essential setup logic:
javascript
Copy
Edit
// core/renderer.js
export const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.xr.enabled = true;
renderer.setSize(window.innerWidth, window.innerHeight);
javascript
Copy
Edit
// core/camera.js
export const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
javascript
Copy
Edit
// core/loop.js
export class Loop {
constructor(camera, scene, renderer) { ... }
start() { renderer.setAnimationLoop(() => this.tick()); }
tick() { ... }
}
2. Scene Modules (/scenes)
Each scene (e.g., Lobby, Garden, Futuristic City) lives in its own module. Scene modules encapsulate:
- Geometry
- Materials
- Lighting
- Scene-specific logic
Example:
javascript
Copy
Edit
// scenes/Scene1.js
export default class Scene1 {
constructor() {
this.scene = new THREE.Scene();
this.loadEnvironment();
this.addObjects();
}
}
3. Systems (/systems)
Contains logic systems (inspired by ECS pattern) such as:
- AnimationSystem
- InputSystem
- PhysicsSystem
- XRSystem
- InteractionSystem
Example:
javascript
Copy
Edit
// systems/AnimationSystem.js
export default class AnimationSystem {
constructor(mixer) { this.mixer = mixer; }
update(delta) { this.mixer.update(delta); }
}
4. Managers (/managers)
Encapsulates application-level logic:
StateManager.js: Tracks user progress or current sceneUIManager.js: Manages on-screen controlsXRManager.js: Handles XR session start/stop logic
5. Utils (/utils)
Reusable functions and wrappers:
loadModel.js: Loads GLTFscreateControls.js: Handles VR or desktop controlscreateUI.js: Mesh UI wrappershelpers.js: Math and general utility functions
Scene Switching Strategy
Use a SceneManager to dynamically load or unload scenes:
javascript
Copy
Edit
class SceneManager {
constructor(renderer) { this.renderer = renderer; }
load(sceneModule) {
if (this.current) this.cleanup();
this.current = new sceneModule();
this.renderer.setAnimationLoop(() => this.current.update());
}
}
Config Management
Use a single config.js file for centralized constants:
javascript
Copy
Edit
export const CONFIG = {
XR_ENABLED: true,
SHADOWS: true,
FOV: 75,
DEBUG_MODE: false,
};
Benefits of Modular Structure
FeatureBenefitScene SeparationEasier scene transitions & testingReusable ComponentsReduces code duplicationSystem Logic IsolationBetter debugging and readabilityAsset ModularityFast swapping of models, shadersConfig CentralizationOne point to control global behavior
Tips for Production Code
- Use ES6 modules: Enables cleaner imports and tree-shaking
- Implement Lazy Loading: For scenes and assets to reduce initial load time
- Enable Hot Reload: For faster development with tools like Vite or Webpack
- Track VRAM Usage: Optimize assets before use in scenes
- Document each module: Describe purpose and usage clearly
Conclusion
A modular codebase is essential for the successful development of performant, extensible WebXR experiences. It ensures your team can collaborate efficiently, track performance issues, and expand the project with confidence as it grows in complexity.