My 3D page shipped 1.9 MB, and 90% of it was never compressed
I always knew 3D pages were heavy. I had never actually measured one.
Then I opened the network panel to see what Midnight Run really ships.
| Resource | Transferred | On disk |
|---|---|---|
| xinyi-night-loop-city.glb | 1,329 KB | 1,329 KB |
| midnight-assets.glb | 385 KB | 385 KB |
| three.module.js | 142 KB | 564 KB |
| MidnightRun.js | 20 KB | 64 KB |
| GLTFLoader.js | 14 KB | 44 KB |
| Total | 1,894 KB |
The two GLB files come to 1,714 KB — 90% of the page.
But what actually stopped me was not that 90%. It was the right-hand column.
The JavaScript is compressed four-fold. The GLB loses nothing.
three.js is 564 KB on disk and arrives as 142 KB — the server strips three quarters of it. GLTFLoader behaves the same: 44 KB on disk, 14 KB on the wire.
The two GLB files transfer at exactly their file size.
The response headers on the live site say why:
| Resource | content-type | content-encoding |
|---|---|---|
| index.js | application/javascript | br |
| kof-3d-studio.glb | model/gltf-binary | (none) |
Cloudflare decides whether to compress from an allowlist of content types. application/javascript is on that list. model/gltf-binary is not.
So the largest file on the whole site is the one file getting no compression at all. Nothing warns you about this. It sat there quietly for months.
Ask the cheapest question first: what would compression even give?
Before reaching for Draco and friends, I wanted to know what plain compression would do. So I ran gzip and brotli over the two files as they are:
| File | Original | gzip | brotli |
|---|---|---|---|
| kof-3d-studio.glb | 2,926 KB | 407 KB | 293 KB (−90%) |
| xinyi-night-loop-city.glb | 1,329 KB | 613 KB | 567 KB (−57%) |
Brotli alone takes ninety percent off the 3D Studio scene.
That number looks absurd, but it makes sense: an uncompressed GLB is a large field of 32-bit floats, neighbouring vertex coordinates are highly similar, and that is exactly what a general-purpose compressor eats for breakfast.
The catch is that on Cloudflare Pages I cannot make it compress that content type. The decision is not mine.
Which leaves one route: bake the compression into the file itself.
Three ways to bake compression into a GLB
The glTF ecosystem offers three common options. I ran each through gltf-transform:
| Method | 3D Studio | Midnight Run city |
|---|---|---|
| Original | 2,926 KB | 1,329 KB |
| Quantise | 993 KB (−66%) | 1,102 KB (−17%) |
| Meshopt | 675 KB (−77%) | 658 KB (−50%) |
| Draco | 489 KB (−83%) | 437 KB (−67%) |
Draco wins on both. On this table alone the conclusion is obvious: use Draco.
The table is missing something.
The decoder is not free
A compressed file means nothing to the browser until a decoder is loaded, and the two decoders are not remotely the same size:
| Decoder | Size |
|---|---|
| Draco (wasm + wrapper) | 245 KB |
| Meshopt | 28 KB |
Draco's decoder is nearly nine times larger. Adding it back flips the answer:
| Page | Method | Models | Decoder | Total |
|---|---|---|---|---|
| 3D Studio | Original | 2,927 KB | — | 2,927 KB |
| Draco | 489 KB | 245 KB | 734 KB | |
| Meshopt | 675 KB | 28 KB | 704 KB | |
| Midnight Run | Original | 1,714 KB | — | 1,714 KB |
| Draco | 505 KB | 245 KB | 750 KB | |
| Meshopt | 771 KB | 28 KB | 799 KB |
On the 3D Studio page meshopt comes out 30 KB ahead of Draco, despite compressing noticeably worse.
Midnight Run goes the other way, with Draco ahead by 49 KB. That page carries two models and more geometry overall — enough to amortise the 245 KB decoder.
As a rule you can carry away: Draco only wins once it saves more than roughly 216 KB over meshopt. Below that, the larger decoder eats the advantage.
Decoders are also cached, so how many 3D pages a visitor sees is another variable. A single one-off visit and a site with five 3D scenes do not have the same optimum.
Textures are the other half
Neither Draco nor meshopt touches textures. In the Xinyi city file, textures are a quarter of the bytes:
| Item | JPEG | WebP |
|---|---|---|
| Embedded textures | 334 KB | 125 KB (−63%) |
| Share of the GLB | 25% | 11% |
Geometry and textures compress independently, so the complete answer does both:
| Treatment | Size |
|---|---|
| Original | 1,329 KB |
| Draco | 437 KB |
| Draco + WebP | 228 KB (−83%) |
One thing I did not expect: those 2.9 MB contain no textures
Halfway through measuring I noticed the 3D Studio file has zero textures in it.
The whole 2,926 KB is geometry — 85,976 vertices with position, normal and UV all stored as 32-bit floats.
That explains why brotli does so well on it, and why quantisation alone removes two thirds. Those floats simply do not need that precision. In a scene eleven metres wide, seven decimal places of position is meaningless.
Had I measured this at the time, that scene would never have been 2.9 MB in the first place.
So what happens now
This is a measurement, not a fix. I have not wired compression into the loading path yet — that touches the initialisation of both 3D scenes, and choosing between Draco and meshopt also depends on whether I am willing to ship two decoders for the sake of a difference between two pages.
But the numbers are clear enough:
| Page | Now | Compressed | Reduction |
|---|---|---|---|
| 3D Studio | 2,927 KB | about 734 KB | −75% |
| Midnight Run | 1,714 KB | about 541 KB | −68% |
On a slow mobile connection that is several seconds. Several seconds before the visitor sees anything at all.
If you want to measure your own 3D page
In this order, because each step costs more than the one before. Stop when it is good enough.
One. Open the network panel and compare the transferred column against the size column. If they match, that file is not being compressed — go and check its content-type.
Two. Check whether your CDN compresses that content type. This is the only optimisation with zero client-side cost, so take it wherever you can.
Three. Measure quantisation once. It needs no decoder at all, and it is startlingly effective on scenes carrying more float precision than they need.
Four. Only then consider meshopt or Draco, and always count the decoder in the total. The method with the best compression ratio is not necessarily the one with the smallest transfer.
Five. Handle textures separately. WebP is close to sixty percent for free.
All of this ran through gltf-transform, one command per method, and it was far quicker than I expected. The slow part was never the compression. It was being willing to look at those two columns in the first place.
The scene measured here: Midnight Run