Three.js From Zero · Article s6-09

S6-09 Asset Streaming

Season 6 · Article 09

Asset Streaming & Addressables

Your game world has 500 models totalling 500MB. You can't download them upfront. You load near-player assets, unload far ones, maintain a cache. Streaming.

1. The load budget

Frame budget at 60fps: 16ms. You can spend ~2ms per frame on asset decode/upload before frame stutter. That's your streaming budget.

2. The priority queue

  1. Visible (on screen, in frustum): highest priority.
  2. Near (within N meters but behind camera): medium.
  3. Far: low.
  4. Out of range: candidate for unload.
function tick() {
  const scored = allAssets.map(a => ({ a, score: priority(a) }));
  scored.sort((x, y) => y.score - x.score);
  let budget = 2; // ms
  for (const { a } of scored) {
    if (!a.loaded) {
      const t0 = performance.now();
      loadOne(a);
      budget -= performance.now() - t0;
      if (budget <= 0) break;
    }
  }
}

3. LRU cache for unload

Memory is finite. Track last access timestamp per asset. When cache is over budget, evict least-recently-used.

if (cacheMB > 200) {
  const victim = cacheEntries.sort((a,b) => a.lastUsed - b.lastUsed)[0];
  victim.dispose();
  cache.delete(victim.key);
}

4. Progressive glTF

glTF's bufferViews mean you can load low-detail geometry first, high-detail later. Not a standard extension yet — build your own tier-1/tier-2 loaders.

5. Live demo — a tiled world streamer

8×8 grid of cells. Camera can pan across. Near cells load their content. Far cells unload. Stats show loaded count.

6. Addressables pattern

Instead of fixed URLs, use string keys:

const assetMgr = {
  async load(key) {
    if (cache.has(key)) return cache.get(key);
    const url = catalog[key];
    const promise = loadUrl(url);
    cache.set(key, promise);
    return promise;
  }
};

await assetMgr.load('enemies.slime');

Key → URL mapping in a JSON catalog. Lets you A/B-test variants, swap assets without code changes.

7. Interest management for networked

Multiplayer add-on: only stream assets for entities your player can currently see. Server tells client which IDs are "visible." Client streams those.

8. HLS / DASH for 3D?

Streaming standards for video exist, not for 3D. Experimental: streamed glTFs with range-requests (HTTP partial content) for tile-based worlds. Cesium 3D Tiles does this for maps.

9. Compressed stream formats

Combine S6-02/03/04:

  • Meshopt-compressed geometry → streams well (small SIMD decode cost per chunk).
  • KTX2 textures → streamed directly to GPU (no JS intermediate).
  • Draco → one-shot loads (higher per-chunk cost).

10. Three.js tools

  • LoadingManager — progress + completion tracking.
  • FileLoader — raw bytes.
  • Cache — built-in (THREE.Cache.enabled = true).
  • Worker: off-main thread GLTFLoader via setPath + worker dance.

11. Takeaways

  • Streaming = prioritize by relevance, load within frame budget.
  • LRU cache to evict stale.
  • Addressables (key→URL) for flexibility.
  • Compressed formats (Meshopt + KTX2) matter doubly when streaming.
  • Never block on asset load during render; schedule into budget.