Draco compression, and the case where it makes your file bigger
Draco routinely takes 90 percent off a glTF file. It also has a failure mode that nobody warns you about: on a scene made of many small meshes it can add a third to the size. Measurements, and what to do instead.
Draco is the default answer to a large GLB, and usually the right one. On a single dense mesh it will take 80 to 95 percent off the vertex data, and every modern glTF reader knows how to undo it. But it has a failure mode that the documentation does not lead with, and it is easy to hit: on a scene made of many small meshes, Draco can make the file bigger.
What it is doing
Uncompressed glTF stores vertex attributes as raw arrays: three float32s per position, three per normal, two per texture coordinate. That is 32 bytes a vertex before you have stored a single triangle, and the numbers are floating point, which compresses badly with a general-purpose algorithm because the low mantissa bits are close to random.
Draco quantizes first. Positions become integers on a grid, typically 11 to 14 bits per axis, which throws away precision you were never going to see and turns near-random low bits into nothing. Then it encodes connectivity with an algorithm that walks the mesh and predicts each next vertex from its neighbours, storing the correction rather than the value. The corrections are small and their distribution is skewed, which is what entropy coding is for.
Both halves of that depend on the mesh being big enough to have structure. That is where the failure mode comes from.
The measurement
Two models, both compressed with the same settings through the compressor here:
| Model | Meshes | Triangles | Before | After Draco |
|---|---|---|---|---|
| Architectural scene | 61 | 2,548 | 944 KB | 78 KB |
| CAD part, per-face meshes | 400 | 25,181 | 1.4 MB | 449 KB |
| Scene of many small meshes | many | few per mesh | 55 KB | 78 KB |
The third row is the one to look at: the compressed file is half again as large as the input. Every Draco-compressed primitive carries its own encoded buffer with its own header and its own tables. On a mesh of a few hundred triangles those fixed costs are a large fraction of the payload, and the prediction has almost nothing to work with because there are barely any neighbours to predict from. Multiply that by a few hundred primitives and the overhead outruns the saving.
The fix is not clever: compress, compare, and keep the compressed result only when it is actually smaller. That is what the compressor here does, and it is worth doing in any pipeline that runs Draco automatically.
A trap in the encoder
Worth knowing if you are writing this yourself. three.js's glTF exporter gives every primitive of a mesh the same attribute accessors and varies only the indices. So if you compress primitives in a loop and rewrite each one's accessors in place, the first primitive you touch corrupts every sibling that shares them.
The symptom is specific and confusing: some primitives compress and some silently do not. In one 61-primitive file only 27 came out compressed. The fix is to read all the source data through a cache before rewriting anything, and to give each compressed primitive detached accessors of its own, because Draco renumbers vertices per primitive anyway. After that, 61 of 61, with a round trip that reports exactly the triangle count that went in.
What to do before reaching for Draco
Draco is the last pass, not the first. In order of how much they pay off:
- Weld duplicate vertices. Formats that store per-face vertices, which is most CAD and anything built from triangle strips, carry three to five times more vertices than the mesh needs. One CAD part here welded 12,395 duplicates out of 34,421 vertices before anything else ran.
- Fix the textures. A 4K PNG normal map is usually larger than all the geometry in the file put together. Re-encoding to WebP at a sane resolution routinely does more than Draco does.
- Simplify, if the model will never be close to the camera. Halving the triangle count halves what Draco has to encode.
- Reorder for the vertex cache. Costs nothing in quality, helps the GPU, and gives Draco's prediction a friendlier ordering to work with.
- Then Draco.
The cost you are paying
Draco is not free at load time. The decoder is a WebAssembly module the reader has to fetch and instantiate, and decoding is real work on the main thread unless you move it to a worker. For a model that loads once and is looked at for a while, that trade is obviously right. For a page that loads forty small models, it may not be.
It also is not universally supported. The extension is declared as required, so a reader that does not implement it will refuse the file rather than draw it wrong. Every current engine has it; older exporters and some CAD viewers do not. If the file is going somewhere you do not control, that is worth knowing before you compress.
You can try both settings on your own model: the compressor reports the before and after and lets you turn Draco off, and the viewer will open the result and tell you whether the triangle count survived.
Sources
Primary references for the claims above. Where a specification exists, it is cited in preference to anybody's summary of it.
- Draco 3D data compression, GoogleThe encoder and decoder, and the quantization options that do most of the work.
- KHR_draco_mesh_compression, Khronos GroupHow a compressed primitive is declared inside a glTF file.
- glTF 2.0 specification, Khronos GroupAccessors, buffer views and the rules a compressed primitive still has to satisfy.
- meshoptimizer, GitHubVertex cache optimisation and simplification, which change what Draco has to work with.