By Papa-Yaw Afari
During development of my VR basketball shot chart visualization using Three.js and WebXR on the Meta Quest 3, memory allocation and performance issues became a central challenge, especially when transitioning from simple 3D scenes to a fully immersive experience with animated basketball shot arcs, real-time UI elements, and shot-by-shot playback.
The goal of the project was to animate and visualize NBA shot chart data using WebXR and Three.js, simulating player shot trajectories on a full-scale court environment. Each shot was represented by a 3D basketball traveling along a curved Bézier path from its origin on the court to the hoop. The system also included:
Real-time visual feedback for made vs. missed shots (color-coded trails, rim hits, airballs).
Interactive scene switching between multiple games (e.g., Jayson Tatum’s 51-point game vs 76ers in 2023 to Carmelo 61 Point Game in 2013).
In-world UI for field goal percentages, shot zone toggles, and play/pause shot replays.
Zone labeling overlays using CSS2DRenderer and later TextGeometry to help users interpret shot locations.
Controller-based navigation and interactions via Meta XR Building Blocks.
The goal was to not just present the data, but simulate it dynamically in VR to provide spatial intuition about where shots were taken and how they traveled—especially when compared between players.
Every basketball shot was instantiated as a new 3D object with its own geometry and material. Animating full games with 30+ shots led to unbounded object creation, resulting in GPU memory overflow and visible performance drops on the Quest headset.
The initial implementation used CSS2DRenderer to label court zones ("Paint", "Midrange", "3PT"). This approach caused performance issues in WebXR mode because of the overhead from overlaying browser-style elements in a continuously re-rendered immersive scene.
Switching between games loaded new shot charts without properly disposing of previous materials, geometries, and event listeners. This led to memory leaks and performance degradation over time—even if it was not immediately visible on desktop.
Triggering all shots at once (as in a “Play All” mode) caused a spike in GPU usage and frame-time jumps. The lack of animation throttling or sequencing overwhelmed the WebXR runtime.
Unlike Unity or Unreal Engine, Three.js and WebXR lack built-in profiling tools. I relied on the Meta XR Performance Hub and Oculus Developer Tools to profile memory usage and rendering performance in real-time. This was critical in tracing down the GPU bottlenecks.
To address these problems, I implemented a series of performance and memory optimizations:
Object Pooling System
Created a pool of reusable basketball objects and trail components that were recycled between shots instead of instantiated anew.
Shot Queuing with Timeout Delays
Converted full-game playback into a timed queue system, staggering animations to maintain performance and reduce simultaneous object movement.
Explicit Disposal of Unused Assets
After each scene/game switch, I ran cleanup routines to dispose of geometries, materials, and textures. Event listeners and animation frames were also unregistered to prevent memory bloat.
Switched to Lightweight Materials
Replaced MeshPhongMaterial with MeshBasicMaterial or MeshLambertMaterial to reduce rendering costs since real-time lighting wasn’t essential.
Transitioned Away from DOM-Based Labels
Replaced CSS2DRenderer labels with TextGeometry-based 3D objects and anchored them to the court, ensuring all visual elements were part of the native render loop.
This project served as a deep dive into the limits of browser-based VR development on standalone headsets. While Three.js and WebXR offer an incredible amount of flexibility, they also demand hands-on resource management. Browser performance does not reflect headset performance. Everything from memory deallocation to object pooling and animation scheduling must be carefully optimized for real-time performance on devices like the Meta Quest 3.For any future immersive data projects, I would recommend early and frequent on-device testing, conservative use of 3D assets, and a strict regime of cleanup and profiling, even when building with lightweight libraries like Three.js.