04/13/2026 // Projects
Building Mouse Trouble for Vibe Jam 2026
How I got a multiplayer Three.js game to load instantly in the browser. Asset pipelines, meshopt compression, texture atlases, and streaming GLBs without a loading screen.
I'm building Mouse Trouble for Vibe Jam 2026. Multiplayer kitchen-stealth game, Two to eight players as mice, Three.js in the browser, PartyKit for networking. You can play it at mouse.ryanfitzpatrick.io. The part of this project that's consumed more of my attention than anything else is load time. Not gameplay, not networking, not the cat AI. Getting a 3D multiplayer game to load instantly in a browser tab.
This is a problem a lot of people building Three.js games for the jam are running into. You have meshes, textures, audio, animation data. A typical game scene can be tens of megabytes before you've done anything wrong. The browser doesn't care that your gameplay is fun. If the initial load takes eight seconds, most people have already closed the tab. For a jam game with no install and no commitment from the player, you get one shot at that first impression.
The approach I landed on is: block on the minimum, stream everything else, and compress aggressively at build time so the runtime never has to wait for something that could have been smaller.
The game has two loading phases. Phase one is blocking. The mouse character and the kitchen room have to be ready before the first frame renders. You can't play without a character and you can't play in an empty void. So those two assets gate the game session. Everything else, the cat, the bunny predator, decorative GLB models scattered around the kitchen, audio, all of that loads in the background after the game is already running.
The mouse model is the critical path asset. It's a rigged GLB with baked animation clips for walk, run, idle, and emotes, plus an eye texture atlas for expressions. The source file goes through a build-time optimization pipeline that does several things. Textures get re-encoded to WebP at 512 by 512, quality 72. Redundant materials and geometry get deduplicated. Animation keyframes get resampled to remove redundancy. Geometry gets quantized: 14-bit positions, 10-bit normals, 12-bit UVs. Then the whole thing gets meshopt compressed. The result is a fraction of the source size and it decompresses on the GPU at load time. The runtime uses Three.js GLTFLoader with the meshopt decoder, and the decoder itself is lazily imported so it doesn't add to the initial bundle if somehow you don't need it.
If the GLB fails to load for any reason, the mouse falls back to a procedural mesh built from basic Three.js geometry. Capsule body, sphere head. It looks rough but the game still works. That fallback has saved me during development more times than I'd like to admit. It means a broken asset pipeline doesn't mean a broken game.
The kitchen is built from a JSON layout format. Every wall, counter, floor tile, and prop is a primitive with a position, rotation, and texture reference. The textures come from atlases. Five spritesheets, ten by ten grids, one hundred textures per sheet. At build time, each atlas gets analyzed and a manifest is generated with UV bounds, average color, hue, saturation, and classification tags for each cell. At runtime the atlases load in parallel, and individual textures are extracted by drawing the relevant cell onto a canvas and creating a CanvasTexture. Materials get cached by cell, repeat, and rotation so duplicate surfaces share the same material instance. One atlas sheet covers an entire kitchen's worth of surface variety in a single texture fetch.
The GLB models in the level, things like pots, bottles, decorative objects, stream in after the room is ready. The room collects every unique GLB asset ID from the layout, deduplicates them, and fires off parallel loads. As each model finishes loading, the room rebuilds the layout incrementally. Objects pop into existence as their meshes arrive. The player doesn't wait for any of this. They're already running around in the kitchen while the props fill in around them. It's not invisible, you can see things appear, but it's dramatically better than a loading bar that holds everything hostage until every last bottle is ready.
Each loaded GLB gets cached by asset ID. If the same model appears twelve times in the layout, it loads once and gets instanced. The chroma key pipeline also runs at load time, replacing green-screen materials with transparency so models composited from multiple sources blend correctly into the scene.
Audio follows the same streaming pattern. Movement sounds, jump effects, and emote audio are prefetched after the game session starts, triggered by user interaction so the browser's autoplay policy doesn't block them. Ambient audio tries multiple formats in sequence, mp3, m4a, ogg, wav, with case variants for cross-platform compatibility. The source audio gets transcoded at build time to mono AAC at 72 kilobits. Small files, fast decode, good enough quality for a game where you're focused on not getting eaten by a cat.
On the bundle side, the Vite config is deliberately simple. ESNext target, no transpilation, no module preload polyfills, no code splitting. The game is a single-page app and the JavaScript bundle is small enough that splitting it would add latency from extra round trips without meaningfully helping parse time. Dev-only features like the level editor and mobile touch controls are dynamic imports so they don't inflate the critical path.
The build pipeline has a caching layer. Each optimization script, GLB compression, image conversion, audio transcoding, checks file modification times and sizes against a cache in .cache/asset-build/. If the inputs haven't changed, the script skips the work. This matters because the full optimization pipeline touches every asset in the project. Running it from scratch takes time. Running it incrementally when you've only changed one texture takes almost none.
The thing I keep coming back to is that none of this is exotic. It's the same principles that apply to any web application. Minimize the critical path. Compress at build time, not runtime. Load what you need, stream what you don't. Cache aggressively. Have fallbacks. The difference with a 3D game is that the assets are bigger and the tolerance for delay is lower. A blog post that takes two seconds to load is fine. A game that takes two seconds to load feels broken. That pressure forces you to think about every byte in a way that makes the whole system better.
Two weeks left in the jam. The game loads fast, plays immediately, and fills in detail as you're already playing. That was the goal from the start and I'm happy with where it landed.
Article FAQ
Article takeaways
- What is the central idea of this post?
- How I got a multiplayer Three.js game to load instantly in the browser. Asset pipelines, meshopt compression, texture atlases, and streaming GLBs without a loading screen.
- Who is this post written for?
- Builders and teams working with projects in production who care about practical architecture and AI-assisted development tradeoffs.
- What should I do after reading this?
- Scan the related posts below, then follow one topic through the existing archive using the search bar.