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 a web-delivery-quality GLB file: typically a single .glb under 3MB that a browser can download, decode, and render fast. It is the biggest single 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.
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 cuts download size but adds parse time. Sharper textures look better but eat GPU memory. More polygons mean a cleaner silhouette but less rendering headroom.
So the pipeline is a series of informed trade-offs, not a single setting you can max out.
The Naive Approach
The export-and-ship workflow that creates most performance problems.
Those two are about what goes into the file. The next two are about how it travels and how big it is on the wire.
Each of these is easy to miss in development, where the file is local and the GPU is generous. They compound, and the last one is how they surface.
glTF as the Standard
glTF (GL Transmission Format) is the standard for web 3D. Three.js loads it natively, it is designed for efficient transmission, and it is extensible via the Khronos extensions system. There are two flavours of the file, and the choice matters.
Ship the GLB file (.glb), the binary glTF format, for production: a single file with geometry, textures, and metadata packed together and no base64 overhead. Keep the .gltf JSON variant for debugging, where being human-readable helps. A plain glTF file with embedded textures is bigger and slower to parse, so it should never reach a real user.
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 a handful of settings that decide your starting file quality and size. Get them right and the optimisation step does less work.
Export Configuration
- Format: glTF Binary (.glb).
- Apply Modifiers: on, so the geometry you see is the geometry you ship.
- Compression: off at export. Run it later through gltf-transform for finer control.
- Textures: JPEG for colour maps (0.85 quality), PNG for normal maps and masks.
- Limit to: selected objects only. Leave out lights, cameras, and 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 tool that does the real work of web 3D optimisation. It operates on glTF files with a composable set of commands, so you can run the full pipeline in one go or step through it for control.
# 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. It is almost always worth it, since the download time saved exceeds the decompression time, but test on target devices.
One more thing: host the Draco decoder files on your own CDN. Don't 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 earns its complexity when a scene loads dozens of textures at once (product configurators with material variants, digital twins with detailed environments). For a single-model scene with two or three textures, the extra build step rarely pays for itself.
Texture Budgets
Pick a resolution from the rendered size of the object, not the source file. The table below is the starting point we reach for; drop a tier whenever the object sits further from the camera.
| 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 user sees the model sharpen without a reload or a stall.
// 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, so a clean source file is worth more than any single optimisation flag.
Geometry hygiene
These four decide how much there is to compress in the first place. Get them wrong and you are compressing waste.
- 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, using Blender's face orientation overlay.
Runtime readiness
The rest are about making the asset safe to use in code and predictable to ship.
- 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, using Blender's stats overlay.
- Test on a real device at target texture resolution before exporting at full res.
The Business Link
None of this is visible in a screenshot. It shows up the moment a real customer opens the page on a three-year-old phone on a train. The pipeline is the difference between a product they wait for and one they abandon.
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 can save every user five or ten seconds on every visit. Across a thousand daily users, that is hours of waiting removed each week, traded for a single day of work.
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 →