Draco vs Meshopt is the decision every Three.js developer hits the moment a .glb model starts costing seconds of load time. Both are glTF geometry compression codecs, both cut download size dramatically, and both are supported natively by Three.js — but they optimise for different things. This guide explains how each works, gives you the real DRACOLoader and MeshoptDecoder setup, a reproducible gltf-transform pipeline, and a decision matrix so you can choose deliberately instead of by habit.
What you'll learn
- What Draco and Meshopt actually compress inside a glTF file, and where they differ.
- The real Three.js wiring for
DRACOLoaderandMeshoptDecoder(three r160). - A reproducible
gltf-transform/gltfpackcompression pipeline. - A qualitative Draco vs Meshopt decision matrix across decode speed, ratio type, CPU/GPU cost, tooling and browser support.
- Production trade-offs, mobile considerations, and when not to compress at all.
What are Draco and Meshopt?
Draco is a geometry compression library from Google, wired into glTF through the KHR_draco_mesh_compression extension. It compresses the connectivity (how vertices form triangles) and quantised attributes together, then reconstructs the mesh at load time inside a WebAssembly decoder. It is a connectivity-aware codec, which is why it tends to produce the smallest geometry payloads for dense, well-formed meshes.
Meshopt (meshoptimizer, by Arseny Kapoulkine) reaches Three.js through the EXT_meshopt_compression extension. Instead of rebuilding connectivity, it quantises vertex attributes, reorders and interleaves them, then applies a byte-level filter that a very small WASM decoder unpacks quickly. The decoded buffers are already quantised and interleaved, so they upload to the GPU with minimal reshaping. Meshopt data also compresses further under the transport layer's gzip or brotli.
Both are geometry codecs. Neither touches your textures — that job belongs to KTX2/Basis (KHR_texture_basisu), which you layer on top of either choice. Understanding that separation is half of the question: you are choosing a geometry decoder, not a whole-asset strategy.
Why model compression matters
An uncompressed .glb for a detailed product model can be several megabytes of raw vertex and index data. On a mid-range phone over cellular, that is the difference between a hero object that appears instantly and one that stalls behind a spinner. Compression shrinks the bytes on the wire; the codec you choose then decides how much CPU you spend turning those bytes back into a mesh. That second cost is exactly where Draco vs Meshopt diverges — and why picking the smaller file is not always picking the faster experience.
How Draco works (and what it costs)
Draco treats the mesh as a graph and compresses the topology alongside quantised positions, normals and UVs. Because it exploits connectivity, it usually yields the tightest geometry files, especially for organic, high-triangle meshes. The price is on the decode side: reconstructing connectivity in the WASM decoder is CPU-intensive, and the decoder module itself is comparatively large, so you serve extra WASM/JS before the first mesh appears. For a single, heavy, cached model that trade is often worth it; for many small models decoded on the main thread it can cause jank.
Real DRACOLoader setup (three r160)
Problem: load a Draco-compressed GLB in Three.js without freezing the main thread. Solution: attach a DRACOLoader to GLTFLoader and point it at the decoder. Three.js ships the decoder in examples/jsm/libs/draco/; serve those files locally or from a pinned CDN.
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const draco = new DRACOLoader();
// Folder must contain draco_decoder.wasm + draco_wasm_wrapper.js
draco.setDecoderPath('/draco/');
draco.setDecoderConfig({ type: 'js' }); // 'js' widest support; 'wasm' faster
draco.preload(); // fetch the decoder early so first model isn't blocked
const loader = new GLTFLoader();
loader.setDRACOLoader(draco);
loader.load('/models/chair.draco.glb', (gltf) => {
scene.add(gltf.scene);
}, undefined, (err) => console.error('Draco decode failed', err));
Why it matters: the decoder runs in a Web Worker pool that DRACOLoader manages, so decode is off the main thread — but the decoder download and worker spin-up still add latency. Calling preload() hides that behind earlier network time. This is a real Three.js API; do not assume the decoder files are bundled with your app — you must copy them into a served path.
How Meshopt works (and why it's fast)
Meshopt does not rebuild topology. It quantises attributes to compact integer types, reorders vertices and triangles for GPU cache efficiency, interleaves them, and applies a reversible byte filter. The MeshoptDecoder unpacks that in a small WASM module — the decode is close to a memory copy, so it is dramatically cheaper per vertex than Draco's connectivity reconstruction. Because the output is already quantised and interleaved, the GPU upload is lean. The one nuance: Meshopt's compressed bytes rely on your server sending gzip or brotli to reach their best ratio, since the codec deliberately produces highly compressible output rather than a fully self-contained small blob.
Real MeshoptDecoder setup (three r160)
Problem: load an EXT_meshopt_compression GLB. Solution: register the decoder on the same GLTFLoader. There is no folder of files to host — the decoder is a single tiny module.
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
const loader = new GLTFLoader();
// Wait until the WASM decoder is ready, then hand it to the loader
await MeshoptDecoder.ready;
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load('/models/chair.meshopt.glb', (gltf) => {
scene.add(gltf.scene);
});
Why it matters: setMeshoptDecoder is the entire integration — the decoder is small enough to inline, so time-to-first-frame is typically shorter than the Draco path. Awaiting MeshoptDecoder.ready guarantees the WASM is initialised before the first decode. Again, a real API — no invented methods.
The compression pipeline with gltf-transform
Whichever codec you land on, the encoding is a build step, not something you do in the browser. The standard tool is glTF Transform. Install the CLI, then always clean the asset before compressing: dedupe, prune unused data, and quantise. Order matters — compress last.
# one-time
npm i -g @gltf-transform/cli
# clean first (safe on both paths)
gltf-transform dedup in.glb a.glb
gltf-transform prune a.glb b.glb
# Path A — Draco geometry compression
gltf-transform draco b.glb model.draco.glb
# Path B — Meshopt geometry compression (quantise + EXT_meshopt_compression)
gltf-transform meshopt b.glb model.meshopt.glb
Why it matters: dedup and prune remove redundancy the codec would otherwise waste effort on, and quantisation (applied inside meshopt, or via gltf-transform quantize before Draco) is where much of the real size reduction comes from. These are real subcommands; run gltf-transform --help to see the full list.
Alternative Meshopt encoder: gltfpack
Problem: you want the reference Meshopt encoder with aggressive quantisation flags. Solution: gltfpack, shipped with meshoptimizer.
# -cc = compress with EXT_meshopt_compression; -tc adds KTX2 texture compression
gltfpack -i in.glb -o model.meshopt.glb -cc
# tune quantisation bits if the model looks faceted after packing
gltfpack -i in.glb -o model.meshopt.glb -cc -vp 14 -vt 12 -vn 8
Why it matters: -cc writes the same EXT_meshopt_compression extension Three.js reads with MeshoptDecoder. The quantisation flags trade a little visual precision for smaller buffers; inspect normals and UV seams after packing, because over-quantising is the most common cause of a model looking wrong once compressed.
Wiring both decoders and choosing at runtime
Problem: a shared loader that can open whichever variant a project ships. Solution: register both decoders once; GLTFLoader reads the file's extensions and invokes the right one. You can also branch on a naming convention.
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
const draco = new DRACOLoader().setDecoderPath('/draco/');
const loader = new GLTFLoader().setDRACOLoader(draco);
await MeshoptDecoder.ready;
loader.setMeshoptDecoder(MeshoptDecoder);
// GLTFLoader auto-selects the decoder from the glTF extensions used.
export const loadModel = (url) => loader.loadAsync(url);
Why it matters: registering both costs almost nothing if a variant is never loaded — the Meshopt decoder is tiny, and Draco's worker only spins up when a Draco file arrives. This is how the AETumi 3D component library keeps one loader that opens either format, so a scene can carry Meshopt hero models and a Draco archival mesh in the same viewer.
Decision matrix: Draco vs Meshopt
The table below is qualitative by design. No compression ratios, file sizes or millisecond timings are quoted, because none were measured in producing this article — inventing them would be worse than useless. Measure on your own models with your own device budget; the relationships below hold generally.
| Axis | Draco (KHR_draco_mesh_compression) | Meshopt (EXT_meshopt_compression) |
|---|---|---|
| Geometry size | Typically the smallest, especially dense/organic meshes | Very good, best when server sends gzip/brotli |
| Decode speed | Heavier — rebuilds connectivity in WASM | Fast — light unpack close to a memory copy |
| Ratio type | Connectivity-aware geometry compression | Quantise + reorder + byte filter |
| CPU vs GPU | More CPU before first frame | Less CPU; outputs GPU-ready quantised buffers |
| Decoder footprint | Larger decoder (extra WASM/JS to serve) | Tiny inline WASM decoder |
| Tooling | gltf-transform draco, Draco encoder | gltf-transform meshopt, gltfpack -cc |
| Three.js support | DRACOLoader (decoder files must be hosted) | MeshoptDecoder (single module, setMeshoptDecoder) |
| Best for | Heavy, static, cached single models | Interactive scenes, many models, fast TTFF |
Interactive: a compressed-model decode viewer
The embed below is a real Three.js scene (r160 via import map, a single requestAnimationFrame loop). It visualises decoding itself: a scattered cloud of vertices streams in and assembles onto the model surface — exactly what a geometry decoder does when it turns compressed bytes back into a mesh. Toggle between the Draco path (connectivity is rebuilt, so vertices lock into place region by region, more slowly) and the Meshopt path (a light unpack, so vertices snap into GPU-ready order almost at once). It does not download compressed files or report any measured numbers; it illustrates the pipeline. It respects prefers-reduced-motion (rendering a single assembled frame) and falls back to a poster image where WebGL is unavailable.
MESHOPT · vertices snap to GPU-ready orderThe heart of the viewer is a vertex-streaming morph: each point interpolates from a scattered position toward its target on the mesh surface as a decode progress value rises from 0 to 1. On the Draco path a per-vertex delay makes the surface rebuild region by region; on the Meshopt path the vertices snap in almost uniformly. One buffer, one requestAnimationFrame — no benchmark, just the shape of a decode.
// points stream from scattered -> target as decode progress `p` rises 0..1
for (let i = 0; i < count; i++) {
const i3 = i * 3;
const local = mode === 'draco'
? clamp01((p - delay[i]) / (1 - maxDelay)) // rebuild region by region
: clamp01(p * 1.05); // light unpack, near-uniform
const e = easeOutCubic(local);
pos[i3] = scatter[i3] + (target[i3] - scatter[i3]) * e;
pos[i3+1] = scatter[i3+1] + (target[i3+1] - scatter[i3+1]) * e;
pos[i3+2] = scatter[i3+2] + (target[i3+2] - scatter[i3+2]) * e;
}
points.geometry.attributes.position.needsUpdate = true; // one buffer, one rAF
Production trade-offs
- Mobile CPU is the constraint. On low-end phones the Draco decode can be the single most expensive step of the load. If your scene loads several models, Meshopt's cheap decode usually wins the real-world experience even when Draco's file is smaller.
- Transport compression is not optional for Meshopt. Serve gzip or brotli. Without it, Meshopt files are noticeably larger than their potential; with it, they close much of the gap to Draco.
- Host Draco decoder files yourself. Relying on a floating CDN version of the Draco decoder is a reliability and versioning risk — pin the version and serve it from your own origin.
- Compression can hurt quality. Over-quantising normals or UVs produces faceting and seam artefacts. Always eyeball the decoded model; the smallest file that looks wrong is not a win.
*.meshopt.glb and *.draco.glb are produced deterministically and your Three.js loader can register both decoders once and open either.When NOT to compress at all
If a mesh is tiny — a few hundred triangles, a simple primitive, an icon-scale object — the decoder download and setup can cost more than the bytes you save. Very small GLBs served with brotli are often fine untouched. Compression earns its keep on medium-to-heavy geometry, not on trivial models. The honest answer for a 20 KB mesh is: neither codec — ship it plain.
GitHub proof: threejs-product-viewer
The AETumi open-source repo AETumiApp/threejs-product-viewer is a Three.js 3D configurator with hotspots, material variants and an exploded view — a natural place for compressed model loading. Verified public files (each returns 200 via the GitHub contents API):
- examples/product-viewer.html — a self-contained viewer using [email protected] via import map with GLTFLoader and OrbitControls.
- examples/README.md — how to run the example.
- GUIDE.md and README.md — configurator overview and integration notes.
The example ships with an uncompressed model for clarity; the DRACOLoader / MeshoptDecoder wiring above is exactly what you add to load a compressed variant of the same asset. GitHub is the proof of the working loader; this article is the explanation of the codec choice around it.
FAQ
Draco vs Meshopt: which should I use for a Three.js model? Use Draco when the smallest possible geometry download is the priority and a heavier CPU decode is acceptable — large, static, cache-once meshes. Use Meshopt when fast decode, quick time-to-first-frame and GPU-friendly buffers matter more, or when you load many models. Most interactive Three.js scenes ship Meshopt by default.
Can I use Draco and Meshopt together in one file? Not on the same mesh — the two geometry extensions are mutually exclusive per primitive. You can mix across files in a project, and both pair independently with KTX2 texture compression.
Does Meshopt decode on the GPU? The decode runs in a small WASM module on the CPU, but it is close to a memory copy and outputs quantised, interleaved buffers that upload to the GPU with little extra work. Draco's connectivity reconstruction is heavier per vertex.
How do I compress a GLB? Install @gltf-transform/cli, then gltf-transform draco in.glb out.glb or gltf-transform meshopt in.glb out.glb. gltfpack -i in.glb -o out.glb -cc is an alternative Meshopt encoder. Dedupe and quantise first, then verify the decoded result in your viewer.
Conclusion and next step
The Draco vs Meshopt choice is not about which codec is "better" — it is about which cost you can afford to pay. Draco spends CPU to buy the smallest geometry file; Meshopt spends a few bytes to buy a near-instant decode and GPU-ready buffers. For most interactive Three.js work — configurators, product viewers, model-led landing pages — Meshopt is the pragmatic default, with Draco held in reserve for heavyweight, cache-once downloads. Wire both decoders once, make compression a build step, and measure on your own models before you commit.
Explore more implementation guides on the AETumi Three.js and WebGL hubs, browse ready-to-use 3D components, read the setup notes in the docs, and see the companion walkthrough on glTF model optimization. When you are ready to ship a compressed model in a real scene, start from a proven AETumi component rather than a blank canvas.
� All guides · See AETumi pricing at aetumi.app
More from the AETumi library
Real, production-ready 3D assets — preview the motion, grab the source.

