Velia lets you sail Lake Tahoe in your browser. I built the first version in three weeks on three.js. It looked good, but it stuttered, even on an M5 Pro MacBook, and phones often failed to load it.
So I built a second version on a small engine made for this one game, beside the first, with both sharing one simulation. It draws a sailing frame about 5× faster on the GPU, with about 10× fewer draw calls and an eighth of the JavaScript. It is not public yet: the live game is still the first version, and the pictures below show what is unfinished.

Why the first version stuttered
I profiled before deciding anything. At 1280×720 the GPU took 13–17 ms per frame while sailing, so a retina MacBook ran at 20–40 fps. On top of that:
- Shaders were still being compiled during play, which gave single frames of 175–495 ms.
- Each frame made about 2 MB of garbage, so the browser paused to collect it.
- Each frame took 265–315 draw calls.
- About 12 MB of terrain data had to arrive before the first frame. 11 of 13 live phone sessions failed to load.
These costs were structural. They come from how a general scene-graph library works, so they were hard to fix from the outside.
What two tiny browser games taught me
Two recent browser games showed me how small this can be: Cold Smoke, a skiing game, and Saltline. Both are under 2 MB (I measured about 1.6 MB each). I studied how they are delivered and drawn, from their public pages only.
- Cold Smoke runs on its own thin WebGPU layer, with a WebGL2 fallback. Every pipeline is built during the loading bar, there is one shared uniform buffer, anything far or slow is baked a little each frame, and a governor lowers resolution instead of dropping frames.
- Saltline is three.js. It shows the value of quality tiers, a pixel budget, compiling shaders up front and a test URL contract.
How I de-risked the rewrite
- Isolate it. The new version lives in its own folder with its own build, tests and reference frames. The first version kept shipping the whole time.
- Extract the simulation first. Wind, weather, sailing physics, the other boats' AI and the time of day moved into a shared module with no library in it. A test fails if anything there imports three.js. I checked the move frame for frame; the storm matched bit for bit over 3,709 frames.
- Milestones with a picture and a budget. An empty frame, then sky and FFT water, real Tahoe terrain, the boat sailing, the real boat models, clouds, forest. Each step came with a comparison at the same camera, measured frame times, and a point where I could stop.

The engine's rules
- No shaders built in play. Every pipeline is created before the first frame, and a late request throws.
- No garbage. Nothing is allocated per frame: one uniform ring buffer, one write, one submit.
- Slow things are spread out. The cloud view is redrawn about once a second, a few rows per frame. Tree images are baked one per frame.
- Culling on the GPU. A compute pass culls about 100,000 trees, then one indirect draw puts them on screen.
- Resolution, not stutter. A governor reads GPU timestamps and lowers resolution before a frame is late.
- The same physics. Both versions sailed 60 s from the same start with the same wind and inputs: heading within 0.5°, heel within 1.2°.

The numbers
| First version (three.js) | New engine | |
|---|---|---|
| GPU frame, 720p, sailing | 13–17 ms | about 2.6–3.5 ms |
| Draw calls | 265–315 | 26–30 |
| JavaScript (gzip) | 607 KB | about 110–120 KB |
| Data before the first frame | about 12 MB | about 1.5 MB in total |
| Clouds | 5.2 ms at low resolution | about 0.2 ms at retina |
| Boat models | 3.1 MB | 0.77 MB |
| Forest data | 20 MB | 1.3 MB |
Measured on an M5 Pro MacBook in headless Chrome, from GPU timestamps. I have not measured a real phone yet.
What it cost, and what went wrong
- There is more code to own: a model loader, materials and an anti-aliasing plan.
- It is WebGPU only. The first version stays as the fallback for other browsers.
- Every visual feature has to be ported again, and the look has to be matched again by eye. The numbers do not catch this.
Two real misses. The sails first read as grey metal, not cloth:

And the island's ground and the clear shallows looked much worse than the first version's. This one is still open:

Each fix meant copying the first version's recipe more exactly.
The lesson
A general engine is the right start for finding the product. Once you know exactly what you draw, and the frame budget is the product, a purpose-built engine wins, as long as you build it beside the shipping version and share one simulation.
I build this way with AI coding agents: how I build.
This post is about Velia: A sailing game on a real lake, with real wind and real-time water and sky. Free, no sign-up.