I built a sneaker configurator that runs in a browser tab — orbit the shoe, switch colourways and materials, watch the price update. It works. But the first version had a problem that had nothing to do with 3D, and everything to do with whether anyone would ever see it.
On a laptop on office wifi that is a two second wait. On a mid-range Android phone on mobile data in Bangladesh, where I built this, it is not a slow experience. It is an abandoned one. The user leaves before the first frame.
This is a write-up of how I got it to 2.4 MB, and of the second problem I hit on the way — that the obvious way to recolour a 3D model destroys the thing you built it to show.
The first thing worth saying: "the model is too big" is not a diagnosis. A glTF file is geometry plus textures plus everything else, and those three respond to completely different treatments. Before optimising anything I wanted to know the split.
That prints a table of meshes, materials and textures with their sizes. In my case the split was roughly: Textures — the large majority of the file Geometry — a meaningful chunk, but smaller than I expected Everything else — noise
This matters because it tells you where to spend effort. Draco is the famous answer and it is a good one, but Draco compresses geometry. If textures are 80% of your file, Draco alone will not save you.
Draco quantises vertex attributes and entropy-codes the result. The important parameter is quantisation bits per attribute, and the important insight is that different attributes tolerate very different precision.
Position needs the most precision — drop it too far and surfaces visibly shift. Normals tolerate far less than people expect; at 10 bits I could not see a difference on curved surfaces under moving light. Texture coordinates sit in between, and getting them wrong shows up as texture swimming at the seams, which is a distinctive and ugly artefact worth knowing by sight.
The defaults are conservative. Walking each value down until you can see the damage, then stepping back one, is worth more than any single setting I could give you, because the right numbers depend on your model's scale and how close the camera gets.
One thing that cost me time: the decoder is itself a download. Serve it yourself rather than from a CDN you do not control, and make sure it is cached — otherwise you have traded model bytes for decoder bytes on every cold load.
The mistake in the source asset was authoring every map at the resolution you would want if the camera could get arbitrarily close. But in a configurator, the camera cannot. There is a defined orbit distance and a maximum zoom, and those set a real upper bound on how much texture detail can ever reach a pixel.
So I re-authored the set at sizes chosen for the actual viewing distance, and separated the maps by how much precision each one needs: Base colour — the map the eye judges. Keeps the most resolution. Normal — needs resolution, but tolerates compression artefacts poorly, so treat it differently from colour. Roughness / metalness / AO — these can go dramatically smaller than feels comfortable. They drive shading response, not perceived detail.
A JPEG texture has to be decoded to raw RGBA before the GPU can use it. A 2048×2048 texture becomes 16 MB in GPU memory regardless of how small the JPEG was. KTX2 stays compressed on the GPU. On a mid-range phone, where GPU memory is the actual ceiling, this is often the difference between running and crashing — and it is invisible if you only measure download size.
With the file small enough, the configurator had a different failure. Swapping a colourway made the shoe look wrong.
The texture was baked with lighting and shadow information in it. Multiplying a saturated colour over that drags everything toward flat. The fabric weave disappears, the baked shadows go muddy, and a red colourway stops reading as the same shoe in red and starts reading as a red silhouette. Which defeats the entire point of showing it in 3D rather than as a photograph.
What worked was separating luminance from chroma and replacing only the chroma. The luminance channel carries the weave, the stitching and the baked shadow — all the information that makes it look like a material. The chroma channel carries the colour, and that is the only part a colourway swap should touch.
That last blend is the part I would not have guessed at the start. Without it, recolouring to blue also turned the white midsole blue. Masking by the texel's own chroma means neutral areas stay neutral and only the already-coloured regions take the new colourway — which is exactly what happens when a factory actually dyes a material.
Measure the split before optimising. I spent my first effort on geometry because Draco is what everyone writes about. The textures were the larger problem.
