Draco and Meshopt Compression

KHR_draco_mesh_compression (Google's Draco 7,495 ) entropy-codes geometry into the smallest files, at the cost of a WebAssembly decode; EXT_meshopt_compression (meshoptimizer 8,458 ) stores quantized streams that decode far faster and shrink further under gzip. glTF Transform applies either to Poly Haven 82,537 's CC0 Mantel Clock 01 (Image Generation APIs's stand-in model, 15,236 vertices):

Compressing the clock's geometry with Draco and with meshoptShell
npm install --global @gltf-transform/cli          # 4.5.0
cp cc0-mantel-clock.glb clock.glb
gltf-transform draco clock.glb clock-draco.glb
gltf-transform meshopt clock.glb clock-meshopt.glb
Output
info: clock.glb (3.13 MB) → clock-draco.glb (2.56 MB)
info: clock.glb (3.13 MB) → clock-meshopt.glb (2.64 MB)

Geometry was only 642 KB; four JPEG textures are the rest (KTX2 and Basis). Decoders come from Babylon 89,603 's CDN on first use and run in web workers:

Loading the three versions and timing eachJavaScript
for (const file of ['clock.glb', 'clock-draco.glb', 'clock-meshopt.glb']) {
  const started = performance.now();
  const container = await BABYLON.LoadAssetContainerAsync(file, scene);
  const vertices = container.meshes.reduce((sum, m) => sum + m.getTotalVertices(), 0);
  console.log(`${file}: ${vertices} vertices in ${Math.round(performance.now() - started)} ms`);
}
Output
clock.glb: 15236 vertices in 142 ms
clock-draco.glb: 15236 vertices in 747 ms
clock-meshopt.glb: 15236 vertices in 121 ms

Draco's time includes fetching its 59 KB wrapper and 192 KB WebAssembly decoder (meshopt's is one 25 KB script; timings are from a shared machine). In production host the decoders yourself: set BABYLON.DracoDecoder.DefaultConfiguration (wasmUrl, wasmBinaryUrl, fallbackUrl) and BABYLON.MeshoptCompression.Configuration = { decoder: { url } }. Prefer meshopt for fast loading, Draco for the smallest download.

A procedural mantel clock beside the download each compression choice adds: Draco's decoder against meshopt'sHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 340px; display: block; touch-action: none; outline: none; }
</style>
<canvas id="view"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@9.28.0/babylon.gui.min.js"></script>
<script>
/*
  The book compresses Poly Haven's Mantel Clock 01 with glTF Transform and loads each file with
  LoadAssetContainerAsync; the Draco and meshopt decoders then come from Babylon's CDN. This preview
  cannot download files, so it builds a clock stand-in in code and charts the sizes the book measured.
  In production, host the decoders yourself:
    BABYLON.DracoDecoder.DefaultConfiguration = { wasmUrl, wasmBinaryUrl, fallbackUrl };
    BABYLON.MeshoptCompression.Configuration = { decoder: { url } };
*/
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.3, 7.8, new BABYLON.Vector3(1.5, 0.9, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.6), scene);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
const mat = (hex) => {
  const m = new BABYLON.StandardMaterial(hex, scene);
  m.diffuseColor = BABYLON.Color3.FromHexString(hex);
  return m;
};

// The clock: a lathed wooden case, a dial and two hands
const profile = [[0.9, 0], [0.9, 0.12], [0.75, 0.2], [0.7, 0.9], [0.62, 1.25], [0.35, 1.55], [0, 1.62]]
  .map(([r, y]) => new BABYLON.Vector3(r, y, 0));
const body = BABYLON.MeshBuilder.CreateLathe('case', { shape: profile, tessellation: 96, cap: BABYLON.Mesh.CAP_END }, scene);
body.scaling.z = 0.35;                                 // flatten into a mantel clock
body.material = mat('#6d4c33');
const dial = BABYLON.MeshBuilder.CreateCylinder('dial', { diameter: 0.9, height: 0.05, tessellation: 64 }, scene);
dial.rotation.x = Math.PI / 2;
dial.position.set(0, 0.95, -0.27);
dial.material = mat('#fff8e1');
const hands = ['#222', '#b5452f'].map((hex, i) => {
  const hand = BABYLON.MeshBuilder.CreateBox('hand' + i, { width: 0.03, height: i ? 0.4 : 0.28, depth: 0.02 }, scene);
  hand.setPivotPoint(new BABYLON.Vector3(0, -(i ? 0.2 : 0.14), 0));
  hand.position.set(0, 0.95 + (i ? 0.2 : 0.14), -0.31);
  hand.material = mat(hex);
  return hand;
});
scene.onBeforeRenderObservable.add(() => {
  const t = performance.now() / 1000;
  hands[0].rotation.z = -t / 12;
  hands[1].rotation.z = -t;
});
const vertices = scene.meshes.reduce((sum, m) => sum + m.getTotalVertices(), 0);

// What each option adds to the first load (KB), as measured in the book
const BARS = [['raw geometry', 642, '#9e9e9e'], ['Draco decoder', 251, '#b5452f'],   // 59 KB JS + 192 KB wasm
  ['meshopt decoder', 25, '#3f7d3a']];                                              // one script
BARS.forEach(([name, kb, hex], i) => {
  const height = kb / 250;
  const bar = BABYLON.MeshBuilder.CreateBox(name, { width: 0.6, depth: 0.6, height }, scene);
  bar.position.set(1.7 + i * 1.6, height / 2, 0);
  bar.material = mat(hex);
  const anchor = new BABYLON.TransformNode('a' + i, scene);
  anchor.position.set(bar.position.x, 0, 0);
  const label = new BABYLON.GUI.TextBlock('', `${kb} KB\n${name}`);
  Object.assign(label, { color: '#333', fontSize: 11, resizeToFit: true });
  ui.addControl(label);
  label.linkWithMesh(anchor);
  label.linkOffsetY = 34;
});
const title = new BABYLON.GUI.TextBlock('', `clock stand-in: ${vertices} vertices · meshopt decodes fastest, Draco downloads smallest`);
Object.assign(title, { color: '#555', fontSize: 12, height: '22px', top: '8px',
  verticalAlignment: BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP });
ui.addControl(title);
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>