Three.js Asset Pipeline

From Blender to Browser in Under 3MB


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.

Default export settings. The glTF contains every vertex, every UV seam, every accidentally-duplicated material. File sizes of 20-50MB are common.
Source-resolution textures. A 4K texture for a table leg that occupies 50 pixels on screen. 64MB of GPU memory for invisible detail.
Single monolithic file. The user waits for the entire model to download before anything appears on screen.
No compression. Raw glTF with embedded base64 textures. The file is 33% larger than necessary from the base64 encoding alone.
First real test on demo day. Performance problems discovered when the application runs on a real device for the first time.

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.

1

Skeleton first

A low-poly silhouette or bounding box (under 50KB). The user sees the shape immediately. Perceived load time drops to near-instant.

2

Base model

The main geometry with simplified textures (under 500KB). The scene is now usable. The user can interact while full quality loads.

3

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.

Remove hidden geometry that the camera will never see (internal faces, back panels against walls).
Merge objects that share a material and will never be individually selected.
Apply all modifiers (subdivision surface, mirror, array, bevel).
Check normals are consistent (Blender's face orientation overlay).
Name every mesh and material for runtime access via getObjectByName().
Remove unused data (materials, textures, vertex groups, shape keys).
Verify triangle count is within budget (Blender's stats overlay).
Test on a real device at target texture resolution before exporting at full res.

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 →
Graphic Swish