Performance

Why Large GLB Files Slow Websites

Trace a GLB through download, decode, parsing, image expansion, GPU upload, compilation, and rendering to fix the stage users actually feel.

A large GLB can delay a website several times before the first useful frame. The browser must discover and fetch it, the loader must parse its structure, compressed data may need decoding, image files expand into pixel memory, GPU resources are created, and shaders may compile. After startup, the scene can remain expensive to render on every frame.

Calling all of this “load time” hides the fix. A 20 MB file on a slow connection has a transfer problem. A 3 MB file containing six 4096 textures can have a decode and memory problem. A model that appears quickly but makes orbit controls stutter has a runtime problem. Measure milestones separately before changing the asset.

The browser cannot render bytes it has not discovered#

The request starts only after the page or application decides that it needs the model. If a client-side bundle must download, parse, execute, mount a component, and then construct a loader before the GLB URL is known, the model request begins late even on a fast network.

Open the browser network timeline with caching disabled and record:

  • navigation start;
  • HTML response start and finish;
  • 3D viewer script discovery and completion;
  • GLB request start;
  • first response byte;
  • GLB response completion;
  • image or decoder requests triggered afterward;
  • first placeholder and first model frame;
  • first successful user interaction.

A preload hint or server-rendered link can move discovery earlier when the asset is essential and known at HTML generation time. Do not preload every product variant. Competing high-priority downloads can delay the page's actual largest content, fonts, or interaction code.

The web.dev resource-loading guidance emphasizes using preload for resources required very soon. A below-the-fold viewer may be better lazy-loaded when it approaches the viewport, while its layout space is reserved to prevent page movement.

Transfer time is more than file size divided by bandwidth#

The simplest lower-bound estimate is bytes divided by usable throughput. A 20 MB decimal file contains 160 megabits. At a steady 10 megabits per second, payload transfer alone needs at least 16 seconds. At 50 megabits per second, it needs at least 3.2 seconds.

Real waits include request scheduling, connection setup when no reusable connection exists, server response delay, bandwidth competition, retransmission, and varying radio conditions. A device that reports a nominal 5G connection may experience a short stall or move to a weaker cell during loading.

Compression headers such as gzip or Brotli usually do not transform an already compressed GLB dramatically. Geometry extensions and JPEG, WebP, or KTX2 images contain their own compressed representations. Measure transferred bytes from the response rather than assuming generic HTTP compression will rescue an oversized asset.

Cache policy changes repeat visits. Content-hashed immutable URLs can be cached for a long time. A URL reused for changing bytes needs revalidation or a short lifetime. Test four scenarios separately:

  1. first visit with an empty HTTP cache;
  2. repeat visit with a warm cache;
  3. application navigation when the parsed asset cache is warm;
  4. a new deployment where the asset URL changes.

The first visit is the acquisition experience; a parsed in-memory cache says nothing about it.

A GLB is parsed into many resources#

The GLB container begins with a 12-byte header and contains a JSON chunk plus an optional binary chunk. The JSON describes scenes, nodes, meshes, accessors, buffer views, materials, textures, skins, and animations. The binary bytes hold the referenced payloads.

Parsing JSON is only one part of loading. A runtime typically:

  • verifies the container and JSON structure;
  • resolves buffer-view and accessor references;
  • creates typed views or copies data for conversion;
  • decodes compressed geometry extensions;
  • creates scene objects and hierarchy links;
  • decodes embedded images;
  • generates missing normals, tangents, or bounds if required;
  • uploads buffers and textures;
  • compiles material shader variants.

Some work can occur in workers; some APIs and GPU operations return to the main thread. A progress bar based only on network bytes can reach 100% while several seconds of decode and upload remain.

Instrument loader phases with the browser Performance API or profiler marks. Record duration and main-thread blocking, not only completion time. A 400-millisecond worker decode may be acceptable; a 400-millisecond main-thread task freezes input and animation.

Geometry compression trades network for computation#

Draco and EXT_meshopt_compression can greatly reduce transferred geometry. The decoder then reconstructs indices and attributes. This is normally worthwhile when network savings exceed decoder cost, but the answer depends on device, cache state, compression settings, and whether decoder code is already available.

Decoder delivery can itself add a request. Host decoder files with stable cache headers and the correct MIME type. Initialize workers before the decode bottleneck if the application knows a compressed model is imminent, but avoid consuming threads and bandwidth unnecessarily on pages that never open the viewer.

Measure:

  • compressed geometry bytes;
  • decoder JavaScript or WebAssembly bytes on a cold cache;
  • decoder initialization time;
  • decode wall time;
  • main-thread blocked time;
  • peak temporary memory;
  • decoded vertex and index buffer size.

A 90% smaller encoded mesh still has its original runtime triangle count after decoding. Compression improves delivery; it does not make each frame render one tenth as much geometry.

Images create the largest expansion surprises#

An embedded 4096 × 4096 JPEG may occupy 2 MB in the GLB, yet its decoded RGBA pixel surface contains 67,108,864 bytes, or 64 MiB. A complete four-channel GPU mip chain is approximately 85.3 MiB before format-specific details. Five such images can create hundreds of MiB from a modest-looking download.

During loading, several representations may coexist:

  • compressed image bytes inside the fetched ArrayBuffer;
  • an encoded image Blob;
  • a decoded CPU pixel surface or browser image object;
  • a GPU texture allocation;
  • generated mip levels;
  • temporary transformed or transcoded buffers.

This overlap drives peak memory. A low-memory tab may be killed during load even if steady-state memory would have fit after temporary objects were released.

Inventory every image by dimensions and semantic use. Test whether 4096 is visible at the closest supported camera. Resize base color, normals, and scalar masks with appropriate filtering. GPU-oriented KTX2 workflows can reduce resident texture cost on supported hardware, but transcode targets vary and must be verified on actual devices.

GPU upload can create a first-frame stall#

Creating JavaScript scene objects does not mean the GPU has every resource ready. WebGL implementations may defer buffer and texture upload until first use. Shader programs are compiled and linked for combinations of material features, lights, skinning, morph targets, fog, clipping, and renderer settings.

The first camera movement can therefore hitch while new materials become visible. A hidden mesh does not necessarily compile until drawn. Precompiling the exact production scene can move the cost earlier, but it does not remove it. Decide whether the product prefers a longer explicit loading state or unpredictable stutters during interaction.

Capture the first 5 seconds after the model appears. Look for:

  • long texture-upload tasks;
  • shader compilation or program-link stalls;
  • sudden GPU memory growth;
  • normal or tangent generation;
  • bounding-volume calculation;
  • environment-map convolution;
  • a burst of garbage collection after temporary buffers release.

Keep the UI responsive during this phase. A loading indicator that freezes is evidence of main-thread monopolization.

Scene construction can be expensive without many bytes#

A GLB with thousands of nodes and primitives can be small on disk yet costly to instantiate. Every node becomes object state; every primitive may create geometry and material bindings; every material variation can require another shader program or draw submission.

Ten thousand 12-triangle parts contain only 120,000 triangles, but traversing, culling, updating, and submitting ten thousand objects can overwhelm the CPU. The opposite—a single 5-million-triangle primitive—has low object overhead but heavy geometry and poor fine-grained culling.

Report structural metrics alongside bytes:

  • scene and node count;
  • mesh and primitive count;
  • material count and distinct shader feature sets;
  • texture count;
  • skin and animation channel count;
  • morph-target count;
  • maximum hierarchy depth;
  • visible objects and draw calls in the initial view.

Combine adjacent compatible pieces when it reduces meaningful overhead, but retain splits that enable culling, animation, interaction, or variant swaps.

Runtime cost repeats every frame#

Once loaded, the renderer performs some combination of hierarchy updates, animation evaluation, frustum culling, skinning, draw submission, vertex processing, rasterization, material shading, blending, shadows, and post-processing.

A large file can contain cheap runtime data, such as high-resolution textures that are sampled efficiently after upload, or expensive runtime data, such as hundreds of transparent material primitives. File size alone cannot predict frame time.

Profile CPU and GPU time separately on target hardware. At 60 Hz, all application work must fit within about 16.67 milliseconds per frame. At 30 Hz, the budget is about 33.33 milliseconds. A frame that averages 14 milliseconds but regularly spikes to 40 still feels unstable.

Test sustained interaction for at least several minutes on thermally limited phones. An initially smooth scene can throttle as the device heats. Record median frame time plus high percentiles such as p95 and p99; averages conceal hitches.

Establish user-visible milestones#

Technical events are useful only when connected to the experience. Define milestones such as:

Viewer reserved: the page has stable layout and a useful poster or placeholder.

Model request started: acquisition begins.

First recognizable frame: enough geometry and material information is visible to understand the object.

Interaction ready: orbit, zoom, selection, and UI respond without blocking.

Full fidelity: optional close-detail textures, animation, or high LOD are present.

Progressive delivery can improve these milestones even when total bytes remain similar. A 300 KB poster or low LOD can appear before a 4 MB inspection asset. The user gains feedback and may never request the highest level if they do not zoom.

Do not label a placeholder as “loaded” in analytics. Record exact definitions and emit them from the same code path across releases.

A diagnostic capture procedure#

  1. Select a low-tier supported phone and a representative desktop.
  2. Use a production build and production hosting configuration.
  3. Clear HTTP, service-worker, and application asset caches.
  4. Apply a repeatable network profile where appropriate.
  5. record the network waterfall and main-thread trace from navigation.
  6. Mark fetch, parse, decode, upload, first frame, and interaction milestones.
  7. Save memory snapshots before load, at peak, and after stabilization.
  8. Orbit through every material to expose deferred compilation.
  9. Record CPU and GPU frame timing in a worst normal view.
  10. Repeat warm-cache and second-navigation tests separately.

Run at least five cold trials. Radio, server, compilation, and scheduling noise make one capture unreliable. Compare medians and the slowest observed experience.

Match the fix to the bottleneck#

If network transfer dominates:

  • resize or re-encode the largest images;
  • compress geometry with a supported profile;
  • remove unused data;
  • configure caching and a content delivery network;
  • avoid eagerly loading variants.

If decode or parse blocks interaction:

  • use workers where the loader supports them;
  • reduce compressed-data complexity and image count;
  • avoid thousands of nodes and primitives;
  • stage resource creation;
  • keep progress UI independent of the blocked work.

If memory peaks:

  • reduce image dimensions and choose GPU-suitable formats;
  • sequence rather than parallelize large decodes;
  • release encoded and temporary buffers promptly;
  • prevent duplicate loader instances and URLs;
  • stream detail only when requested.

If steady-state frames are slow:

  • profile draw calls, triangles, fragments, transparency, shadows, and shader cost;
  • add LOD and culling;
  • combine compatible primitives or instance repetition;
  • simplify the dominant pass;
  • test resolution scaling for fragment-bound scenes.

Shipping checklist#

  • The GLB request starts at an intentional point in page loading.
  • Cold transferred bytes and cache behavior are recorded.
  • Download, decode, parse, upload, compilation, and first frame have separate timings.
  • Decoder payload and initialization are included in the cold test.
  • Image inventory includes dimensions and estimated decoded memory.
  • Peak memory is measured during load, not only afterward.
  • First interaction remains responsive while optional detail loads.
  • Initial-view materials are compiled without surprise hitches.
  • Node, primitive, material, and draw counts are reported.
  • Sustained frame timing passes on the lowest supported device.
  • Analytics distinguishes first recognizable frame from full fidelity.
  • The chosen optimization improves the user-visible milestone it targets.

Sources and further reading#

A large GLB is not one problem. It is a chain of acquisition, conversion, allocation, and rendering work. Label those stages, measure them on real devices, and optimize the one that controls the user's next meaningful moment.

Keep learning

Related guides

Performance10 min

How to Diagnose a Slow 3D Model

Use a repeatable decision tree to isolate network, decode, memory, CPU submission, vertex, fragment, shader, and lifecycle bottlenecks.

3d profilingdiagnosticswebgl performance
Performance10 min

How to Build Mobile 3D Performance Budgets

Turn device tiers, frame deadlines, memory limits, network conditions, thermal behavior, and scene measurements into enforceable mobile 3D budgets.

mobile 3dperformance budgetframe time
Performance10 min

3D Load Time vs. Runtime Performance

Separate startup acquisition and preparation from recurring CPU and GPU frame work, then verify that each optimization improves the intended experience.

load timeframe timeprofiling