Three.js From Zero · Article s6-09
S6-09 Asset Streaming
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
- Visible (on screen, in frustum): highest priority.
- Near (within N meters but behind camera): medium.
- Far: low.
- 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.