Hiding loading transitions in a browser game
2025-12-18 ・ ~4 min read
The problem: objects pop in while loading
- The game starts while some objects are still missing
- If GLB models/textures are not ready, objects appear later suddenly
- It looks bad and hurts UX
- I wanted an easy way to register “loading work”
- Long tasks should be trackable without knowing loader internals
- GLB loading, texture loading, physics world init, etc. should be unified
Solution: `SceneLoadTracker`
- Track Promises per scene
- Register with
track()and keep a counter of pending work
- Register with
- Wait for idle before starting render/gameplay
waitForIdle()waits until pending work becomes 0- Prevents pop-in by starting after all loads are finished
- Simple API
- Just pass the Promise to
track()
- Just pass the Promise to
SceneLoadTracker Implementation Details
SceneLoadTracker is implemented as a singleton and manages loading state per scene ID.
export class SceneLoadTracker {
private static instance: SceneLoadTracker | null = null;
private stateBySceneId = new Map<string, SceneLoadState>();
track<T>(sceneId: string, promise: Promise<T>): Promise<T> {
const state = this.ensure(sceneId);
state.total += 1;
state.pendingPromises.add(promise);
promise.finally(() => {
state.completed += 1;
state.pendingPromises.delete(promise);
this.notify(sceneId);
});
return promise;
}
waitForIdle(sceneId: string): Promise<void> {
const snap = this.snapshot(sceneId);
if (snap.pending === 0) {
return Promise.resolve();
}
return new Promise((resolve) => {
const unsubscribe = this.subscribe(sceneId, (s) => {
if (s.pending === 0) {
unsubscribe();
resolve();
}
});
});
}
}
Usage Example
// Track GLB model loading
const loader = new GLTFLoader();
const loadPromise = scene.trackLoad(
loader.loadAsync('model.glb')
);
// Track texture loading
const texturePromise = scene.trackLoad(
new THREE.TextureLoader().loadAsync('texture.jpg')
);
// Wait for all loads to complete
await scene.waitForLoadingIdle();
// Start the game from here
scene.start();