41.0 MB → 12.8 MB, same geometry, one honest trade-off

A measured optimization pass on a public heavy glTF scene. Both viewers below load a real file over the network and report what they actually took. Drag to compare — the geometry is untouched — renderVertexCount is 4,497,216 in both files, verified with gltf-transform inspect.

SOURCE · 41.0 MB
loading 41 MB…
OPTIMIZED · 12.8 MB
loading…

What the numbers are

buildfilerender vertsupload vertstexturesnormalsglass
source41.0 MB4,497,216417,0282048 px JPEGf32correct
this pass12.8 MB4,497,216567,8601536 px JPEGf32correct
Khronos reference11.5 MB4,497,216266,184KTX2 / ETC1Sf32correct
aggressive variant7.0 MB2,945,184317,1871024 px JPEGi8broken

Two places where this pass is beaten, and they belong here rather than in a footnote. The reference build ships GPU-compressed KTX2 textures, so it wins on video memory at a similar file size. And its upload vertex count is 266,184 against my 567,860 — the join step traded shared geometry for fewer draw calls, which costs GPU memory. On a phone that trade can go the wrong way. Which build is right depends on whether the bottleneck is bandwidth, VRAM or draw calls — that is a decision to make per project, not a default to copy.

The part that cost a rebuild

The first pass came out at 7.0 MB — smaller than the official build. It was wrong. Meshopt encodes normals as 8-bit octahedral, and that quietly destroys smooth KHR_materials_transmission glass: the transparent spheres on the pawns turned into dark faceted blobs. File size said success; the render said otherwise.

Draco keeps normals at f32 and the glass survives. The lesson is not "use Draco" — it is that a size number alone cannot tell you whether an optimization worked. Every pass here was checked by rendering both versions side by side, not by reading a report.

The pass, in full

gltf-transform optimize src.glb raw.glb \
    --compress false --texture-compress false --simplify false   # dedup, join, prune only
gltf-transform copy raw.glb un/scene.gltf                        # externalise textures
# textures: cap at 1536 px, re-encode (q92 for normal/AO maps, q88 for albedo)
gltf-transform copy un/scene.gltf packed.glb
gltf-transform draco packed.glb out.glb --quantize-normal 12 --quantize-position 14

Texture pass: 18.05 MB → 9.81 MB. Geometry and buffers carry the rest. Total wall-clock, start to finish: under a minute on a laptop.

What this does not claim

This is one scene, measured once, on a desktop connection. A real project needs its own numbers: your texture budget, your target device, and whether transmission, skinning or morph targets are in play — each of them changes which steps are safe. The scene here is ABeautifulGame from the Khronos sample assets, CC-BY 4.0, chosen because it is heavy, public, and ships with an official optimized build to check against.