Platforms

How to Optimize GLB Models for Three.js

Build, load, profile, cache, render, and dispose production GLB assets in Three.js with supported compression and measurable budgets.

A Three.js-ready GLB is more than a file that GLTFLoader can open. It needs an extension profile the application configures before loading, scene structure that produces sensible draws, texture formats supported by target devices, stable first-use behavior, and explicit resource ownership when the user leaves.

Optimize the artifact and the loader together. A compressed GLB fails if its decoder is missing. A small model still stutters if shader variants compile during the first orbit. A correctly disposed route behaves differently from one that retains every texture.

Establish the renderer target#

Record the Three.js revision, renderer, supported browsers, lowest device tier, pixel ratio policy, lighting, shadows, post-processing, and interaction frame target. WebGLRenderer now targets WebGL 2; older advice written for WebGL 1 may no longer match current releases.

Use a production build. Development framework checks and hot reload distort timing and lifecycle. Test the actual canvas dimensions: a 390 × 844 CSS display at pixel ratio 3 can request more than 8.8 million drawing-buffer pixels, before antialiasing and intermediate targets.

Set an intentional pixel-ratio cap based on measured quality. Halving width and height reduces pixel count by 75%, often the strongest control for fragment-bound scenes.

Define milestones for request start, first recognizable frame, interaction ready, and full detail. Capture CPU and GPU frame milliseconds rather than one desktop fps reading.

Configure GLTFLoader for the file profile#

GLTFLoader is an addon imported separately. Its official documentation lists supported glTF extensions and exposes configuration for Draco, KTX2, and meshopt decoding.

Before requesting a compressed asset:

  • attach DRACOLoader for KHR_draco_mesh_compression;
  • set its versioned decoder path and cache headers;
  • attach a meshopt decoder for EXT_meshopt_compression;
  • attach KTX2Loader for KHR_texture_basisu;
  • call KTX2Loader detectSupport with the active renderer;
  • configure cross-origin and content-security policies;
  • handle initialization and load failures visibly;
  • pin compatible Three.js and decoder versions.

Do not configure every decoder because a tutorial does. Each adds code, initialization, caching, and deployment surface. Choose the profile from measured candidates and destination support.

Host WebAssembly with the correct MIME type. Verify worker creation under production CSP. A local development server can hide both failures.

Verify hosting and failure behavior#

Serve GLB and decoder responses with stable content types, content lengths, cache policies, and cross-origin headers. Use content-hashed URLs for immutable public assets; do not overwrite bytes under a year-long cached URL. Test aborted navigation so an obsolete load cannot attach its scene after the user has selected another product.

Handle network, parse, decoder, texture-transcode, and unsupported-feature errors separately. Preserve a readable poster and page controls when 3D fails. Log the asset revision, loader revision, extension set, and failure stage without exposing signed URLs or user data. Retry only transient acquisition failures; repeatedly retrying an invalid GLB wastes bandwidth and can leave duplicate workers or partial scene state.

Test slow response, offline, 404, truncated bytes, decoder 404, worker rejection, and WebGL context loss. A fast happy path is not production readiness.

Inspect the loaded scene#

GLTFLoader returns the default scene, all scenes, animations, cameras, parser, and asset metadata. Traverse the default scene and report:

  • Object3D count and maximum hierarchy depth;
  • Mesh, SkinnedMesh, and InstancedMesh count;
  • geometry groups and material assignments;
  • triangle and rendered-vertex totals;
  • unique geometry, material, texture, and skeleton identities;
  • opaque, transparent, and double-sided materials;
  • animation and morph-target count;
  • bounding box, dimensions, and outliers.

One visible product can contain 200 Mesh objects and 80 materials. File bytes do not show the CPU traversal and draw structure.

Use renderer.info after representative frames to observe calls, triangles, points, lines, geometries, textures, and programs. Counters may include shadows and other render passes. Reset and sample consistently.

Optimize scene structure before loading#

One glTF primitive commonly becomes a BufferGeometry group or mesh draw with one material. Reduce duplicate materials and tiny primitives in the source pipeline. Preserve splits required for transparency, independent animation, selection, culling, and product variants.

For repeated identical parts, use EXT_mesh_gpu_instancing when the exporter and loader path preserve it, or build an InstancedMesh application-side. One hundred bolts should not require 100 unique geometry buffers and submissions if only transforms differ.

Do not merge a kilometer of static scene into one bound. Fewer draws can produce worse culling. Cluster spatially and benchmark close, typical, and wide views.

Flatten unexplained static transform nodes only when application semantics do not need them. Preserve joints, animated pivots, attachment points, and named interaction anchors.

Choose geometry delivery#

First simplify geometry to its screen-space need. Then compare core or quantized buffers, Draco, and meshopt at equivalent visual precision.

Record:

  1. total and geometry bytes;
  2. cold decoder bytes;
  3. initialization and decode time;
  4. main-thread blocking;
  5. decoded BufferAttribute component types;
  6. GPU buffer estimate;
  7. triangles and vertices after decode;
  8. runtime frame time.

Compression does not reduce decoded triangles. A 90% smaller Draco payload still renders the original topology. Add LOD or simplify when vertex work is the problem.

Quantized attributes may remain compact or be expanded by a tool path. Inspect geometry attributes after load. Position, normal, tangent, UV, skin, and morph arrays all contribute.

Use KTX2 for runtime texture goals#

KTX2Loader parses KTX 2.0 containers and transcodes Basis Universal content to a GPU-supported compressed format. detectSupport must run before loading so the target format matches the renderer and device.

KTX2 is not a reason to retain unnecessary dimensions. A 4096 texture contains 16 times the texels of 1024. Select dimensions from maximum projected use, then encode color, normal, and scalar content with suitable settings.

Log the selected runtime format and estimate mip memory. Compare base-color gradients, alpha edges, normal-map direction, and roughness highlights on target devices.

Keep a fallback profile for destinations that do not support the same extension. In an application you control, a clear compatibility failure is better than a silently blank product.

Dispose KTX2Loader when its transcoder resources are no longer required, following current API behavior. Do not dispose a shared loader while other loads are active.

Control first-use compilation#

New material feature combinations can compile shaders when first rendered. A model may appear, then hitch as the camera reveals transmission, skinning, morphs, clipping, or another light/shadow combination.

Configure production scene lighting and environment, then use WebGLRenderer compileAsync where appropriate. The official API uses KHR_parallel_shader_compile when available and resolves when the scene can render without unnecessary compilation stalls.

Precompile likely variants, not every hypothetical material. Include the wait in the interaction milestone. Compare an explicit 250-millisecond preparation with an unannounced 250-millisecond first-orbit freeze.

Move the camera through all material regions after preparation and capture the first 5 seconds. Texture upload, environment processing, or bounds calculation can create other one-time hitches.

Render on demand when the scene is idle#

Product viewers often spend long periods still. Rendering 60 frames per second while nothing changes wastes power and can heat a phone.

Use an invalidation model: request a frame after control changes, resize, animation progress, loading completion, material variant change, or UI-driven transform. Continue a loop only while animation, damping, XR, or temporal effects require it.

For continuous rendering, WebGLRenderer documentation recommends setAnimationLoop for compatibility. In XR it coordinates the device loop. Do not create multiple animation loops through repeated component mounts.

Measure idle CPU/GPU activity and battery impact. Ensure async loading schedules a frame when the model becomes ready.

LOD and visibility#

Three.js LOD associates objects with distance thresholds and supports hysteresis. Distance is simple but ignores scale, field of view, and viewport. For a consistent product viewer, calculate screen coverage or derive distances from fixed camera policy.

Each level must share origin, dimensions, material appearance, and interaction mapping. Use hysteresis to prevent toggling. Load higher levels on intent if downloading all levels up front defeats initial performance.

Raycasting through LOD needs stable semantic results. If selection depends on child mesh names that disappear, provide a separate interaction proxy or mapping.

Turn off frustumCulled only when bounds or special rendering genuinely require it. Fix bad bounds rather than disabling culling globally.

Transparency, shadows, and lighting#

Transparent materials can sort poorly and add overdraw. Use opaque for solid regions and alphaTest or glTF MASK behavior for hard cutouts when suitable. Limit double-sided materials.

Review which meshes cast and receive shadows. A 200000-triangle product rendered into 3 shadow maps can multiply vertex work. Use a simpler shadow representation or selective casting when image quality permits.

Environment maps need memory and processing. Reuse one environment rather than cloning it per asset. Scene.environment and background can create internal renderer resources; include them in lifecycle tests.

Avoid adding real-time lights to fix incorrectly authored PBR values. Compare under a neutral environment and preserve consistent tone mapping and color management across candidates.

Resource ownership and disposal#

Removing an Object3D from the scene does not dispose its GPU resources. Three.js requires explicit disposal of BufferGeometry, Material, Texture, WebGLRenderTarget, and other resources. A material disposal does not dispose its textures because textures can be shared.

Define ownership at load time. Track resources introduced by one asset before other objects are attached to its hierarchy. Reference-count shared environments, textures, geometries, and skeletons.

When an asset truly leaves:

  • remove roots and event listeners;
  • stop AnimationMixer actions and release references;
  • dispose unique geometries;
  • dispose unique materials;
  • dispose textures not shared elsewhere;
  • close ImageBitmap CPU resources when owned;
  • dispose skeletons only when not shared;
  • clear asset-cache entries under the intended policy.

Calling dispose on a resource still in use causes recreation or breakage. Never traverse a scene at unload and blindly dispose everything if common resources are shared.

Cycle between the heaviest routes 10 times. renderer.info counts should stabilize, allowing for documented internal resources.

Cache by stable identity#

Loading the same URL can reuse application or loader caches depending on architecture. Query strings, signed URLs, and object cloning can defeat identity. Separate immutable asset identity from temporary authorization URL.

Cache the parsed asset when reuse is likely, but bound the cache. A commerce viewer that visits 30 products should not retain all 30 indefinitely on mobile.

Cloning scene nodes can share geometry and materials, but skinned meshes need a skeleton-aware clone path. Decide whether variant changes mutate a shared material or create an isolated material instance. Accidental mutation can recolor every visible instance.

Record cache hits, unique GPU resource counts, and eviction. Warm in-memory navigation is not a substitute for cold-load testing.

Diagnose performance with controls#

If frames are slow:

  • halve drawing-buffer width and height to test fragment pressure;
  • replace materials with one cheap opaque material;
  • force a low LOD to test geometry;
  • disable shadows and post effects independently;
  • hide half the objects to test traversal and submission;
  • freeze animation and morph updates;
  • inspect draws by pass and shader programs.

If loading is slow, separate request, response, parse, geometry decode, image transcode, scene creation, upload, and compilation. Change one variable per candidate.

Use browser performance tools with renderer.info and target-device observation. Do not optimize by counters alone.

Shipping checklist#

  • Three.js, loader, and decoder versions are pinned and tested.
  • The GLB extension profile matches configured loaders.
  • Decoder paths, MIME, CSP, workers, and caching pass production tests.
  • Loaded scene counts and bounds are inspected.
  • Texture dimensions precede KTX2 encoding decisions.
  • Geometry codecs are compared at equivalent precision.
  • compileAsync or another explicit strategy controls first-use stalls.
  • Idle viewers stop rendering when product behavior allows it.
  • LOD thresholds and hysteresis match screen behavior.
  • Transparent and shadow passes have measured budgets.
  • Asset ownership, sharing, caching, and disposal are explicit.
  • Cold load, repeated navigation, and sustained mobile frames all pass.

Sources and further reading#

Three.js optimization spans the exported GLB and its full browser lifecycle. Configure exactly the extensions you ship, profile decoded resources and passes, prepare the first interaction, render only when useful, and release every asset according to explicit ownership.

Keep learning

Related guides

Platforms10 min

How to Optimize 3D Models for Ecommerce Product Viewers

Design measurable 3D product assets and delivery tiers that improve ecommerce inspection without sacrificing page speed, accessibility, visual truth, or conversion analytics.

ecommerce3d product viewerweb performance
Platforms10 min

How to Optimize 3D Models in React Three Fiber

Optimize model loading, rendering, state, memory, frameloops, and responsive quality in React Three Fiber without turning React updates into frame-time work.

React Three FiberThree.jsReact
Platforms10 min

How to Optimize 3D Models for Android Scene Viewer

Prepare GLB assets for Android Scene Viewer with documented model limits, correct scale and orientation, mobile texture budgets, AR launch links, and device testing.

AndroidScene ViewerARCore