A 3D model that looks perfect in Blender can be unusable in the browser. A 50MB file that loads in three seconds on your development machine takes 30 seconds on mobile data. Textures exported at 4096x4096 consume 64MB of GPU memory for a component that renders at 200px on screen. A model with 2 million polygons renders at 4fps on an integrated GPU.
The asset pipeline is the process of turning authoring-quality 3D assets into web-delivery-quality assets. It is the single biggest determinant of whether a Three.js application feels fast or slow, and it is the step most teams skip until they discover their application is unusable on real devices.
The Constraint: Competing Budgets
Web 3D assets must satisfy four budgets simultaneously. Violating any one of them degrades the experience.
Download Size
Under 3MB for the initial load on mobile. Every MB above that adds seconds on 4G connections. Users leave before the scene appears.
GPU Memory
Integrated GPUs (most laptops, all mobile) have 1-4GB of VRAM shared with the system. A single 4096x4096 RGBA texture consumes 64MB uncompressed on the GPU.
Polygon Count
Integrated GPUs struggle above 500K-1M triangles with standard materials and lighting. Complex shaders reduce this threshold further.
Parse Time
The browser must parse the file, decode compressed data, and upload buffers to the GPU. A 10MB Draco file might decompress to 40MB, taking 500ms on mobile CPUs.
These budgets interact. Draco compression reduces download size but increases parse time. Higher-resolution textures improve visual quality but consume GPU memory. More polygons improve silhouette quality but reduce rendering headroom. The pipeline is a series of informed trade-offs.
The Naive Approach
The export-and-ship workflow that creates most performance problems.
glTF as the Standard
glTF (GL Transmission Format) is the standard for web 3D. It is supported natively by Three.js, designed for efficient transmission, and extensible via the Khronos extensions system. Use glTF Binary (.glb) for production: a single binary file with no base64 overhead. The .gltf JSON variant is useful for debugging (human-readable) but should not be shipped to production.
The Pipeline
The complete path from authoring to browser delivery.
Author
Blender, Revit, SketchUp
Clean Up
Remove hidden geo, merge mats
Export glb
Named nodes, applied modifiers
gltf-transform
Draco, dedup, resize, WebP
Split
Progressive loading files
Deploy
CDN with cache headers
Blender Export Settings
The Blender glTF exporter has settings that significantly affect file quality and size.
Export Configuration
Format: glTF Binary (.glb). Apply Modifiers: On. Compression: Off at export (use gltf-transform for better control). Textures: JPEG for colour maps (0.85 quality), PNG for normal maps and masks. Limit to: Selected objects only. Do not export lights, cameras, or helper objects. Custom Properties: On, if you need userData for runtime identification.
Name everything. Name every mesh and material in Blender. Three.js accesses nodes by name via scene.getObjectByName(). Unnamed nodes get auto-generated names that change between exports, breaking runtime code that references them.
gltf-transform CLI
gltf-transform is the essential tool for web 3D optimisation. It operates on glTF files with a composable set of commands.
# Install
npm install -g @gltf-transform/cli
# Full optimisation pipeline (one command)
gltf-transform optimize input.glb output.glb
# Or step by step for control:
# 1. Deduplicate accessors and textures
gltf-transform dedup input.glb deduped.glb
# 2. Draco-compress geometry (80-90% size reduction)
gltf-transform draco deduped.glb compressed.glb
# 3. Resize textures (max 1024px for web)
gltf-transform resize compressed.glb resized.glb --width 1024 --height 1024
# 4. Convert textures to WebP (30-50% smaller than JPEG)
gltf-transform webp resized.glb final.glb --quality 80
# 5. Convert to KTX2 for GPU-compressed textures
gltf-transform ktx2 resized.glb final-ktx2.glb --compress uastc
Before/after file sizes from a real asset pipeline:
| Stage | File size | Notes |
|---|---|---|
| Raw Blender export | 24.3 MB | Uncompressed, 4K textures |
| After dedup | 18.1 MB | Removed duplicate buffers |
| After Draco | 4.2 MB | Geometry compressed |
| After texture resize | 1.8 MB | Textures downsized to 1024px |
| After WebP | 1.1 MB | WebP replaces JPEG/PNG |
That is a 95% reduction. The visual difference at web viewport sizes is negligible.
Draco Compression
Draco (Google's geometry compression) reduces geometry data by 80-90%. Three.js includes a DracoLoader that decompresses on the client.
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
dracoLoader.preload();
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load('/models/product.glb', (gltf) => {
scene.add(gltf.scene);
});
The trade-off: Decompression takes CPU time. On mobile, a heavily compressed model can take 200-500ms to decompress. This is almost always worth it (download time saved exceeds decompression time), but test on target devices. Host the Draco decoder files on your own CDN. Do not rely on the Three.js CDN for production.
KTX2 and GPU-Compressed Textures
Standard textures (JPEG, PNG, WebP) decompress to raw RGBA on the GPU. A 1024x1024 JPEG that is 200KB on disk becomes 4MB in GPU memory. KTX2 uses GPU-native compression formats (UASTC, ETC1S) that stay compressed in GPU memory.
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('/basis/');
ktx2Loader.detectSupport(renderer);
gltfLoader.setKTX2Loader(ktx2Loader);
KTX2 is most valuable when the application loads many textures (product configurators with material variants, digital twins with detailed environments). For a single-model scene with 2-3 textures, the complexity may not be justified.
Texture Budgets
| Texture purpose | Max resolution | Format |
|---|---|---|
| Hero product (fills viewport) | 2048x2048 | WebP or KTX2 |
| Standard model | 1024x1024 | WebP |
| Environment/background | 1024x1024 | WebP or HDR |
| Normal maps | 1024x1024 | PNG or KTX2 |
| Thumbnail/distant objects | 512x512 | WebP |
The rule: size textures for their rendered size, not their source size. A texture that never renders larger than 256 pixels on screen should not be 2048 pixels in the file.
Progressive Loading
A single monolithic file makes users wait. Progressive loading shows content immediately and refines it as more data arrives.
Skeleton first
A low-poly silhouette or bounding box (under 50KB). The user sees the shape immediately. Perceived load time drops to near-instant.
Base model
The main geometry with simplified textures (under 500KB). The scene is now usable. The user can interact while full quality loads.
Full quality
High-resolution textures, environment maps, additional detail meshes. Loaded in the background. The upgrade is seamless.
// Progressive loading with priority
async function loadModel() {
// Phase 1: skeleton (instant feedback)
const skeleton = await gltfLoader.loadAsync('/models/product-skeleton.glb');
scene.add(skeleton.scene);
// Phase 2: base model (replaces skeleton)
const base = await gltfLoader.loadAsync('/models/product-base.glb');
scene.remove(skeleton.scene);
scene.add(base.scene);
// Phase 3: high-res textures (background upgrade)
const textures = await Promise.all([
textureLoader.loadAsync('/textures/product-diffuse-2k.webp'),
textureLoader.loadAsync('/textures/product-normal-2k.webp'),
]);
base.scene.traverse((child) => {
if (child.isMesh && child.material.map) {
child.material.map = textures[0];
child.material.normalMap = textures[1];
child.material.needsUpdate = true;
}
});
}
For product configurators, load the base model and default material first. Variant materials load on demand when the user selects them. For digital twins, load the building structure first, then equipment, then data overlay.
Model Preparation Checklist
Run through this before every export. Most pipeline problems are authoring problems that compression cannot fix.
getObjectByName().The Business Link
First impressions are load times. A product configurator that takes 8 seconds to load on mobile loses the customer before they see the product. A digital twin that consumes 2GB of GPU memory crashes the operator's browser after an hour. A geospatial visualisation with unoptimised terrain tiles stutters during the board presentation.
The pipeline runs once per asset update. The performance benefit is permanent. A day spent on asset optimisation saves every user seconds on every visit. For applications with hundreds or thousands of daily users, the cumulative time saved is substantial.
Optimise Your 3D Assets
We build Three.js applications with optimised asset pipelines: Blender-to-browser workflows, Draco compression, KTX2 textures, and progressive loading. Fast on mobile, sharp on desktop.
Let's talk about your project →