Every asset here is a small program rather than a frozen mesh, which raises a fair question: is that slower than loading a GLB? These are the measured answers, on this catalogue, with the script that produced them in the repo.
No. This is the number most people expect to go the other way.
| Asset | Triangles | Same shape, 25× | Different shape every call | Knob varied |
|---|---|---|---|---|
| Street lamp | 422 | 0.79 ms | 0.61 ms | tallness |
| Toy rocket | 618 | 2.48 ms | 2.27 ms | tallness |
| Windmill | 1,811 | 4.56 ms | 4.11 ms | tallness |
| Convertible 60s | 4,784 | 10.36 ms | 9.78 ms | stretch |
The factory runs the same code whatever the values are, so a configurator offering a hundred variants costs exactly what one offering a single shape costs. Only the number of calls matters.
No. It runs when a parameter changes, and never otherwise.
| Page state | Window | Rebuilds | What is happening |
|---|---|---|---|
| Loaded, untouched | 5 s | 0 | render loop running, factory idle |
| Slider in motion | 2.5 s | 42 | one rebuild per frame, by design |
| Slider released | 5 s | 0 | back to idle immediately |
Once built, drawing it is identical to drawing a GLB: the same geometry and the same single material are already on the GPU. The build cost is paid per change, not per frame.
Same thing on screen. The difference is what you pay upfront and what you can still change.
| Asset | Module | gzipped | GLB | gzipped | Build cost |
|---|---|---|---|---|---|
| Street lamp | 11.1 KB | 4.1 KB | 45.7 KB | 6.3 KB | 0.79 ms |
| Toy rocket | 11.3 KB | 4.0 KB | 66.3 KB | 7.9 KB | 2.48 ms |
| Windmill | 25.2 KB | 8.4 KB | 194.2 KB | 26.6 KB | 4.56 ms |
| Convertible 60s | 24.8 KB | 7.7 KB | 365.4 KB | 32.5 KB | 10.36 ms |
Module sizes are the comment-stripped copies actually served, gzipped as your server would send them. The module came out smaller on the wire for every asset measured here and produces any variant; the GLB needs no build step and opens in Unity, Godot and Blender. Neither is faster to render.
Calling the factory once per copy is the mistake that actually costs you.
| 20 copies of Convertible 60s | CPU | Distinct geometries | Why |
|---|---|---|---|
createAsset() per copy |
199.20 ms | 40 | every copy uploaded to the GPU separately |
.clone() from one build |
3.30 ms | 2 | buffers shared, uploaded once |
Build once and clone(), or use THREE.InstancedMesh
for many copies of one shape. This is the only case where the factory can genuinely hurt.
Every asset in the catalogue uses one vertex-coloured material and no textures, which means any set of them collapses into a single geometry.
| Scene | Assets | Meshes | Triangles | Merge time | Draw calls after |
|---|---|---|---|---|---|
| cottage, tree, well, barrel, fence, lamp | 6 | 10 | 4,712 | 0.40 ms | 1 |
import { mergeAssets } from 'https://polyfork.dev/cdn/merge.mjs';
const { merged, dynamic } = mergeAssets([cottage, tree, well]);
scene.add(merged); // one draw call, static
dynamic.forEach(d => scene.add(d)); // wheels, doors, still animatable
Position everything before merging: world transforms are baked in. Rig-declared parts come back separately so they can still move.
When a slider does rebuild every frame, the geometry was never the expensive part.
| Per rebuild, counted in the browser | Before | After |
|---|---|---|
| Shader program links | 1 | 0 |
| Shader compiles | 2 | 0 |
three.js reference-counts a compiled GPU program against the materials using it. Dispose the last material holding a program and the program is deleted, so the next rebuild has to compile and link again, and a link is a synchronous stall inside the driver. Keeping one material instance per distinct configuration removed it entirely. If you drive a knob live: dispose geometry, reuse materials.
mergeAssets. One draw call for the lot.clone() or InstancedMesh. Never call the factory in a loop.Measured 2026-07-28 on node v22.22.1, three r185.
median of 25 calls per asset, after 8 warm-up calls, single core. CPU figures come from tools/bench.mjs in the repo;
the shader counts were taken in a real browser by patching the WebGL context, and the idle-rebuild
counts by watching the scene graph across animation frames. Absolute milliseconds depend on your
hardware, so treat the ratios as the durable part.