AETumi — AI-native 3D web platform for Three.js, WebGL and interactive websites
News & Guides

Draco vs Meshopt for Three.js model compression

September 9, 2026 · AETumi

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.

Direct answer (TL;DR). Pick Draco when the smallest geometry download is the priority and a heavier CPU decode is acceptable — large, static, high-poly meshes that are cached and viewed once. Pick Meshopt when fast decode and quick time-to-first-frame matter more, when you load many models, or when you also ship KTX2 textures — its decoder is tiny and its output is GPU-friendly. If you only remember one rule: Meshopt is the sensible default for interactive Three.js scenes; Draco is the specialist for heavyweight single-model downloads.

What you'll learn

  • What Draco and Meshopt actually compress inside a glTF file, and where they differ.
  • The real Three.js wiring for DRACOLoader and MeshoptDecoder (three r160).
  • A reproducible gltf-transform / gltfpack compression 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.

Encode and decode pipeline for Draco and MeshoptOne source glTF splits into two horizontal encode lanes, a Draco lane and a Meshopt lane, each passing through its glTF extension and Three.js decoder before both converge on GPU vertex buffers.SourceglTF / GLBDRACO LANEMESHOPT LANEConnectivity +attribute compressKHR_draco_mesh_compressionDRACOLoaderheavier WASMQuantise · reorder· byte filterEXT_meshopt_compressionMeshoptDecodertiny WASMGPU vertexbuffers
Two encode lanes from one source glTF — Draco compresses connectivity, Meshopt quantises and filters — both ending at Three.js decoders and GPU buffers.

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.

CPU versus GPU-oriented decode pathsTwo segmented time lanes. The Draco lane spends a long segment rebuilding connectivity on the CPU before GPU upload, while the Meshopt lane does a short unpack and reaches GPU upload sooner. Qualitative, not a benchmark.Meshopt reaches first frameDraco reaches first frameDRACODownloadHeavy CPU · rebuild connectivityGPU uploadMESHOPTDownloadLight unpackQuantised → GPU uploadtime →Relative CPU work before first frame — Meshopt reaches GPU upload sooner (qualitative, not a benchmark).
The decode paths compared qualitatively: Draco spends a long CPU segment rebuilding the mesh; Meshopt does a light unpack and reaches GPU-ready buffers sooner.

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.

AxisDraco (KHR_draco_mesh_compression)Meshopt (EXT_meshopt_compression)
Geometry sizeTypically the smallest, especially dense/organic meshesVery good, best when server sends gzip/brotli
Decode speedHeavier — rebuilds connectivity in WASMFast — light unpack close to a memory copy
Ratio typeConnectivity-aware geometry compressionQuantise + reorder + byte filter
CPU vs GPUMore CPU before first frameLess CPU; outputs GPU-ready quantised buffers
Decoder footprintLarger decoder (extra WASM/JS to serve)Tiny inline WASM decoder
Toolinggltf-transform draco, Draco encodergltf-transform meshopt, gltfpack -cc
Three.js supportDRACOLoader (decoder files must be hosted)MeshoptDecoder (single module, setMeshoptDecoder)
Best forHeavy, static, cached single modelsInteractive scenes, many models, fast TTFF
Decision tree for choosing Draco or MeshoptA decision tree starting at the model, branching on whether fast decode or smallest bytes matters most, then on how many models load, ending at Meshopt or Draco recommendations.glTF model to shipWhat matters most?fast decode / TTFFsmallest bytesLean toward MeshoptConsider DracoMany models / mobile CPU?yes → back to MeshoptMeshopt default
A practical decision tree — most interactive Three.js scenes end at Meshopt; Draco wins for heavy, static, cache-once downloads.

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 order
Vertices streaming in and assembling into the model surface — a real Three.js r160 scene, not a benchmark. Toggle the decode path to change how the mesh reconstructs.

The 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.
AETumi Expert Note — decode budget over file size. When we build product viewers for the AETumi library, we optimise for time-to-first-interactive-frame, not for the smallest byte count. That usually means Meshopt for anything the user rotates or configures, and Draco reserved for a heavy archival mesh that is downloaded once and cached hard.
AETumi Expert Note — separate geometry from textures. Geometry codec and texture codec are independent decisions. Pair either Draco or Meshopt with KTX2/Basis for textures; on most real models the texture payload dominates, so KTX2 often moves the load-time needle more than the geometry codec you argue over.
AETumi Expert Note — bake the choice into the build. Make compression a CI step (gltf-transform or gltfpack) keyed off a naming convention, so *.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):

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.

Browse all Three.js assets →