Tutorial
gltf-transform optimize: A Practical Guide to Shrinking glTF/GLB Files
· 8 min read

gltf-transform optimize input.glb output.glb runs a full optimization pipeline in one command: it removes duplicate data, instances repeated meshes, flattens and joins the scene to cut draw calls, welds and lightly simplifies geometry, prunes unused data, caps textures at 2048 px and compresses geometry with Meshopt. For the smallest files on the web, the usual recipe is --compress draco --texture-compress webp, plus --texture-size 1024 when your textures don't need 2K.
This guide covers installation, what each step does, every optimize flag with its default, the individual commands for finer control, the scripting API and the errors people hit most. Everything here was checked against @gltf-transform/cli 4.5.0.
Install gltf-transform
You can run the CLI without installing anything through npx, or install it globally:
# Run once without installing
npx @gltf-transform/cli optimize input.glb output.glb
# Or install globally, then use the gltf-transform command
npm install --global @gltf-transform/cli
gltf-transform --version- Node.js 20 or newer is required.
- sharp, the image library used for resizing and WebP, AVIF, JPEG and PNG encoding, is installed automatically as a dependency of the CLI.
- KTX2 texture compression is the exception: it calls the
ktxtool from KTX-Software (version 4.4.0 or newer), which you install separately.
What gltf-transform optimize does, step by step
The optimize command chains existing gltf-transform functions in this order. Each step can be switched off with its flag, except the first.
- dedup: merges duplicate accessors, meshes, materials, textures and skins.
- instance: turns a mesh reused 5 or more times into GPU instances (
EXT_mesh_gpu_instancing). - palette: packs solid-color material values into small palette textures and merges the materials, which helps CAD and low-poly scenes.
- flatten: flattens the node hierarchy.
- join: joins meshes that share a material to reduce draw calls.
- weld: merges identical vertices (required for simplification and Draco).
- simplify: reduces triangles with meshoptimizer, using a very small default error of 0.0001 (0.01% of the mesh size).
- resample: removes redundant animation keyframes without changing the motion.
- prune: removes anything not used by a scene, including unused vertex attributes, and turns single-color textures into material factors.
- sparse: stores mostly-zero arrays (common in morph targets) as sparse accessors.
- texture compression: resizes textures to the maximum size and re-encodes them.
- geometry compression: Meshopt by default, or Draco or quantization.
To see where the weight is before and after, run gltf-transform inspect model.glb. It lists every mesh, material and texture with sizes and resolutions, and the extensions the file requires.
optimize flags and defaults
From gltf-transform optimize --help. Boolean flags take true or false (--simplify false); the --no-simplify form also works.
| Flag | Default | What it does |
|---|---|---|
--compress | meshopt | Geometry compression: draco, meshopt, quantize or false |
--meshopt-level | high | medium or high |
--texture-compress | auto | webp, avif, ktx2, auto (re-encode in the original format) or false |
--texture-size | 2048 | Maximum texture width and height in pixels; aspect ratio is kept |
--simplify | true | Simplify meshes with meshoptimizer |
--simplify-error | 0.0001 | Error tolerance as a fraction of the mesh extent |
--simplify-ratio | 0 | Target fraction of vertices to keep (0 to 1) |
--simplify-lock-border | false | Lock topological borders while simplifying |
--instance / --instance-min | true / 5 | GPU instancing and the number of copies needed |
--palette / --palette-min | true / 5 | Palette textures and the minimum number of unique values |
--flatten | true | Flatten the scene graph |
--join | true | Join meshes to reduce draw calls |
--join-meshes / --join-named | true / true | Whether distinct meshes and named meshes and nodes may be joined |
--weld | true | Merge equivalent vertices |
--resample | true | Losslessly deduplicate animation keyframes |
--prune | true | Remove unreferenced data (see also --prune-attributes, --prune-solid-textures) |
--sparse | true | Sparse storage for zero-filled arrays |
Practical optimize examples
Smallest file for three.js or <model-viewer>
gltf-transform optimize input.glb output.glb \
--compress draco \
--texture-compress webp \
--texture-size 1024Here is what different settings did to the Khronos DamagedHelmet sample (3.77 MB, five 2048 px JPEG textures) with CLI 4.5.0. It is one texture-heavy model, so treat the numbers as an illustration, not a promise:
| Command | Output size |
|---|---|
optimize (all defaults) | 2.62 MB |
--texture-compress webp | 1.44 MB |
--compress draco --texture-compress webp | 1.34 MB |
--compress draco --texture-compress webp --texture-size 1024 | 442 KB |
--compress draco --texture-compress avif --texture-size 1024 | 288 KB |
On this model the texture settings mattered far more than the geometry codec, which is typical for PBR assets. For geometry-heavy files such as scans or CAD, the --compress choice matters more. See Draco vs Meshopt for how to pick.
Keep the scene structure intact
Flattening and joining are great for draw calls but they remove the hierarchy and merge nodes. If your app finds nodes by name, animates individual parts or toggles visibility per mesh, turn them off:
# Keep hierarchy and separate meshes
gltf-transform optimize input.glb output.glb --flatten false --join false
# Or still join, but leave named meshes and nodes alone
gltf-transform optimize input.glb output.glb --join-named falseControl simplification
The default simplification is conservative. To cut triangles harder, set a ratio and a larger error limit. To keep geometry exactly as modeled (CAD, measured scans), switch it off:
# Aim for 50% of vertices, allowing up to 0.1% error
gltf-transform optimize input.glb output.glb --simplify-ratio 0.5 --simplify-error 0.001
# No simplification at all
gltf-transform optimize input.glb output.glb --simplify falseThe simplifier stops early when it hits the error limit, so the ratio is a target, not a guarantee. If you see cracks along seams, add --simplify-lock-border true or lower the error.
Leave textures alone, or use KTX2
--texture-compress false skips the texture step entirely, and with it the --texture-size resize. --texture-compress ktx2 produces GPU-compressed KTX2 textures (UASTC for normal, occlusion and metal/roughness maps, ETC1S for the rest). The CLI's own help sums up the trade-off: KTX2 optimizes GPU memory and performance, while WebP and AVIF optimize download size. KTX2 also needs KTX-Software installed.
Maximum viewer compatibility
Draco and Meshopt both need a decoder in the viewer. --compress false writes plain geometry any glTF viewer can read. --compress quantize sits in between: it stores smaller integer attributes via KHR_mesh_quantization with no decoder library, but the loader still has to support that extension.
Individual commands for finer control
Every step of optimize is also its own command, with more options. Run gltf-transform <command> --help for the full list. The most useful:
inspect: sizes, resolutions and extensions (--format mdorcsvfor reports).validate: runs the Khronos glTF validator.dedup,prune,weld,simplify(--ratio,--error),flatten,join,instance.resize:--widthand--heightas maximums; textures are never enlarged.webp,avif,jpeg,png: re-encode with--quality, filtered by--slots(for example"baseColorTexture").etc1sanduastc: KTX2 + Basis Universal texture compression.draco:--quantize-position(default 14 bits),--quantize-normal(10),--quantize-texcoord(12),--encode-speedand--decode-speed(5).meshopt(--level medium|high) andquantize.
A manual pipeline on the same helmet looked like this:
gltf-transform resize input.glb step1.glb --width 1024 --height 1024 # 3.77 MB → 1.25 MB
gltf-transform webp step1.glb step2.glb --quality 80 # → 928 KB
gltf-transform draco step2.glb output.glb # → 448 KBThe CLI reads and writes both formats: give the output a .gltf extension to get JSON with separate .bin and image files, or .glb for a single binary.
Scripting with @gltf-transform/core and functions
For build pipelines, use the same functions from Node.js. Install the packages plus the encoders you need:
npm install @gltf-transform/core @gltf-transform/extensions @gltf-transform/functions draco3dgltf meshoptimizer sharpimport { NodeIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
import { dedup, prune, weld, simplify, textureCompress, draco } from '@gltf-transform/functions';
import { MeshoptSimplifier } from 'meshoptimizer';
import draco3d from 'draco3dgltf';
import sharp from 'sharp';
// Register extensions and the Draco encoder/decoder modules.
const io = new NodeIO()
.registerExtensions(ALL_EXTENSIONS)
.registerDependencies({
'draco3d.decoder': await draco3d.createDecoderModule(),
'draco3d.encoder': await draco3d.createEncoderModule(),
});
await MeshoptSimplifier.ready;
const document = await io.read('input.glb');
await document.transform(
dedup(),
prune(),
weld(),
simplify({ simplifier: MeshoptSimplifier, ratio: 0.75, error: 0.001 }),
textureCompress({ encoder: sharp, targetFormat: 'webp', resize: [1024, 1024], quality: 80 }),
draco(),
);
await io.write('output.glb', document);Save it as an .mjs file (or set "type": "module") so top-level await works. Two details trip people up: draco() needs the draco3dgltf modules registered on the IO, and textureCompress() needs sharp passed as encoder. Without it, a fallback is used and most quality options are ignored. To use Meshopt instead of Draco, await MeshoptEncoder.ready, register it on the IO as 'meshopt.encoder' (and MeshoptDecoder as 'meshopt.decoder'), then call meshopt({ encoder: MeshoptEncoder }). Skipping the registration fails at write time.
Common problems
KTX2 fails with "Command failed: command -v ktx"
You asked for KTX2 (--texture-compress ktx2, etc1s or uastc) without KTX-Software on your PATH. Depending on the platform, the error may instead read Command "ktx" not found. Install KTX-Software 4.4.0 or newer and check that ktx --version works in the same terminal.
sharp fails to install or load
sharp ships prebuilt binaries per platform, installed as optional dependencies. Use Node.js 20.9 or newer, don't disable optional dependencies, and reinstall on the machine that runs the code: a node_modules copied from macOS won't work on Linux. For cross-platform builds, the sharp install docs explain flags like npm install --cpu=x64 --os=linux --libc=glibc sharp. Don't bundle sharp with webpack, Vite or esbuild; mark it as external.
The optimized model doesn't show up in the browser
The file now requires a decoder. In three.js you'll see errors like THREE.GLTFLoader: No DRACOLoader instance provided or setMeshoptDecoder must be called before loading compressed files. Attach a DRACOLoader, MeshoptDecoder or KTX2Loader to your GLTFLoader, as shown in how to load GLB files in three.js.
Parts of the model moved, merged or lost their names
That is flatten and join at work. Re-run with --flatten false --join false or --join-named false. Run the result through the glTF validator to confirm the file is still valid.
When a hosted optimizer is easier
The CLI is the right tool if you're comfortable with Node.js and want Meshopt or KTX2. If you'd rather not install Node, sharp and KTX-Software on every machine, or you need non-developers to compress files, compress-glb.com runs on gltf-transform on our servers. It covers Draco, texture resizing, WebP/AVIF/JPEG/PNG re-encoding and geometry passes such as weld, simplify, instancing, flatten and merge meshes, with saved presets, batch uploads and a REST API for pipelines. It doesn't offer Meshopt or KTX2 output; for those, stick with the CLI.
FAQ
What does gltf-transform optimize do by default?
It runs dedup, instance, palette, flatten, join, weld, simplify, resample, prune and sparse, re-encodes textures in their original format at up to 2048 px, and compresses geometry with Meshopt. Every step except dedup can be turned off with its flag.
How do I set the texture size in gltf-transform optimize?
Use --texture-size 1024 (or any pixel value). It is a maximum for width and height, the aspect ratio is kept, and it only applies when --texture-compress isn't false.
Should I use Draco or Meshopt with gltf-transform?
Draco usually gives smaller static meshes; Meshopt decodes faster and also compresses animation and morph targets. Pick whichever your viewer supports. The Draco vs Meshopt comparison goes deeper.
Does gltf-transform optimize reduce quality?
Some steps are lossy: simplification, quantization in Draco and Meshopt, and texture re-encoding and resizing. The defaults are conservative (a simplification error of 0.0001, for example), but compare the output with the original side by side before shipping.
Is gltf-transform free?
Yes. glTF Transform is open source under the MIT license, and the CLI and libraries are free to use in commercial projects.


