GPU Memory for 3D Assets: A Practical Budgeting Guide
Estimate textures, geometry, render targets, shadows, transient allocations, and duplicated resources before a real-time 3D scene exhausts device memory.
GPU memory failures rarely announce themselves with a neat “budget exceeded” message. A browser tab may reload, textures may disappear, a WebGL context may be lost, or the operating system may kill the process. The asset that downloaded successfully can still fail because its renderable form is much larger than its encoded file.
Budget the complete scene, not a GLB in isolation. Textures and mesh buffers share memory with shadow maps, multisampled color and depth targets, environment maps, post-processing buffers, decoder intermediates, browser resources, and the rest of the page.
File bytes are the wrong starting proxy#
A 12 MB GLB can expand past 300 MiB. An 80 MB GLB can contain efficiently compressed geometry that occupies less memory than expected after streaming only one level. Container bytes answer network and storage questions; resource dimensions and component types answer memory questions.
Create three columns for every resource:
- encoded bytes retained by the application;
- CPU-side decoded or typed-array bytes;
- GPU allocation estimate.
Add a fourth column for transient peak. During loading, the encoded GLB ArrayBuffer, decompressed mesh arrays, decoded image pixels, transcode output, GPU objects, and old scene may overlap. The peak causes failure even if garbage collection later reaches a safe steady state.
Texture allocation arithmetic#
For an uncompressed 2D texture, top-level bytes are approximately width × height × bytes per texel. Common examples:
- R8: 1 byte per texel;
- RG8: 2 bytes;
- RGB8: often treated as 3 bytes conceptually, though implementations may use other alignment or internal storage;
- RGBA8: 4 bytes;
- RGBA16F: 8 bytes;
- RGBA32F: 16 bytes.
A 2048 × 2048 RGBA8 top level contains 16,777,216 bytes, or 16 MiB. The same dimensions in RGBA16F contain 32 MiB. A 1024 × 1024 RGBA32F environment or simulation target also contains 16 MiB before mips or faces.
A full mip chain for a square power-of-two texture adds the geometric series 1/4 + 1/16 + 1/64 and so on, approaching one third of the base level. A 16 MiB base becomes about 21.33 MiB. Non-square and block-compressed formats need exact level calculations, but one-third is a useful RGBA estimate.
Cube maps have six faces. A 1024 × 1024 RGBA16F cube base uses 1024 × 1024 × 8 × 6 = 48 MiB; a full mip chain approaches 64 MiB. Environment preprocessing can retain an original, a radiance cube, an irradiance representation, and temporary render targets unless lifecycle is controlled.
GPU block compression#
Block-compressed formats store a fixed number of bytes per block rather than per independent texel. At 8 bits per pixel, a 4096 × 4096 top level is roughly 16 MiB; at 4 bits per pixel it is about 8 MiB. Compare that with 64 MiB for RGBA8.
KTX2 and Basis Universal are delivery containers and transcode workflows, not one guaranteed resident format. A device might choose ASTC, ETC, BC, or another supported target. Quality and alpha behavior depend on the mode and texture content. Normal maps and color maps may need different encoding settings.
Log the selected target and calculate its block allocation for every mip. Tiny mip levels still occupy at least one compression block. For a large texture the difference is negligible; for thousands of tiny textures, minimum blocks and object overhead can add up.
GPU compression helps resident memory and sampling bandwidth, while PNG and JPEG primarily help transfer. A 1 MB JPEG decoded into RGBA8 does not occupy 1 MB on the GPU.
Geometry buffer estimates#
List every final attribute. For 150,000 vertices:
- float position, 12 bytes: 1.8 MB;
- float normal, 12 bytes: 1.8 MB;
- float tangent, 16 bytes: 2.4 MB;
- float UV, 8 bytes: 1.2 MB;
- 16-bit joints plus normalized weights, 16 bytes: 2.4 MB;
- subtotal: 9.6 MB decimal.
If 250,000 triangles use 32-bit indices, 750,000 indices add 3.0 MB. Geometry totals about 12.6 MB before morph targets.
Six position-and-normal morph targets stored as 24 float bytes per vertex add 6 × 150,000 × 24 = 21.6 MB. Morph data is now larger than the base geometry. A report that lists only triangles will miss it.
Quantized attributes may remain smaller in GPU memory when the runtime preserves their integer component types. Some importers expand them to floats. Inspect the loaded BufferAttribute arrays or engine resource report. Do not assume the transfer representation survives unchanged.
Index width is per primitive. A primitive with at most 65,535 addressable vertices can use 16-bit unsigned indices, halving its index memory relative to 32-bit. Splitting a mesh only to cross that threshold may introduce draw calls and duplicate boundary vertices, so compare the full trade.
Copies, clones, and resource identity#
One geometry referenced by 100 instances can occupy one set of buffers. One hundred deep clones can occupy 100 sets. In application code, visual identity does not prove resource sharing.
Log unique geometry, material, texture, and render-target object identities. In Three.js, cloning an Object3D hierarchy does not necessarily deep-copy every geometry and material, but later code may clone or replace them. Loading the same bytes through URLs that differ by query string can create separate cache entries.
Common duplication sources include:
- a new loader and cache scope for each component mount;
- signed URLs that change on every request;
- deep-cloning meshes to change one material parameter;
- texture transforms implemented by copying texture objects;
- separate GLBs embedding the same environment or material images;
- retaining old scenes during route transitions;
- renderer recompilation during hot reload or configuration changes.
Make shared ownership explicit. A resource cache should have stable keys, reference counts or a clear lifecycle, and tests for repeated navigation.
Render targets can exceed asset memory#
The frame itself consumes memory. For a 1920 × 1080 RGBA8 color target, one sample is about 7.91 MiB. A 32-bit depth target adds another 7.91 MiB. Four-times multisampling can multiply storage before resolve, depending on implementation.
At 3840 × 2160, RGBA8 is about 31.64 MiB per target. A deferred renderer with multiple geometry buffers, depth, history buffers, bloom levels, and antialiasing can allocate hundreds of MiB before any product textures.
WebXR renders high-resolution views for two eyes, often with device-specific framebuffer sizing. Post-processing chains may allocate ping-pong targets. Dynamic resolution and foveated rendering can change the actual numbers. Query renderer dimensions and pixel ratio during the target session.
Inventory:
- main color and depth;
- multisample attachments and resolves;
- shadow maps and cascades;
- reflection and probe targets;
- post-processing inputs, outputs, and history;
- picking or object-ID buffers;
- environment convolution targets;
- temporary resize or capture targets.
A memory plan that allocates 90% to assets leaves no safe space for these systems.
Shadow memory#
A 2048 × 2048 depth shadow map with 4 bytes per sample is about 16 MiB. Four cascades approach 64 MiB. Six point-light cube faces at the same resolution approach 96 MiB. Actual formats and packing vary, but the order of magnitude matters.
Review shadow resolution per light, not only globally. A small fill light does not need a 4096 map. Static or baked lighting may replace some real-time shadows. Far geometry can use a lower-detail caster, and tiny objects may not need to cast at all.
Reducing a product texture from 4096 to 2048 saves about 64 MiB including mips in RGBA8 terms. Reducing three oversized shadow maps may save more without touching asset appearance. Profile the whole renderer.
CPU memory still affects GPU loading#
JavaScript ArrayBuffers, decoded images, WebAssembly heaps, worker copies, and application state live outside GPU memory but compete for total device resources. A decoder may allocate an output buffer, then the loader copies it into a typed array, then the renderer uploads it. Transferable objects and careful ownership can reduce copies, but implementation details need measurement.
An embedded image can exist inside the original GLB buffer and as a new Blob or ImageBitmap. Releasing application references does not guarantee immediate collection. Avoid launching ten large decodes simultaneously if sequencing three at a time keeps the peak within budget with acceptable latency.
Record process or browser memory where available, plus known typed-array totals. GPU-only counters do not explain a tab killed by combined system pressure.
WebGL context loss#
WebGL can lose its context for several reasons, including GPU resets, resource pressure, tab lifecycle, or browser policy. Applications should listen for context loss and restoration events, prevent default behavior when restoration is supported, and rebuild resources safely.
Context restoration is not a substitute for a budget. If restoring reloads the same 600 MiB scene, it may fail again. Log device, asset set, render dimensions, and recent resource changes when loss occurs, without collecting sensitive data.
Test intentional loss with the WEBGL_lose_context extension in development where available. Verify the page shows a useful fallback and does not duplicate resources on restoration.
The MDN WebGL best-practices guide recommends deleting objects eagerly and losing contexts when they are no longer needed. Explicit disposal makes lifecycle more predictable than waiting solely for garbage collection.
Set a scene-level envelope#
Public web applications cannot reliably know a fixed dedicated VRAM capacity or claim all of it. Other tabs, the browser compositor, operating system, and integrated GPU may share memory. Use conservative empirical envelopes by device class.
Build a worst normal scene:
- largest supported viewport and pixel ratio;
- maximum simultaneous product or character set;
- required LODs and variants;
- all normal shadows and post effects;
- animation and interaction active;
- route transition or asset replacement in progress;
- representative browser UI and page content.
Measure low-tier devices until failure, then choose a production budget with margin. If a 2 GB phone survives at a 280 MiB estimated scene but becomes unstable near 360 MiB, do not set the budget to 359 MiB. OS and browser variation require headroom.
Define tiers by features, not only asset dimensions. A low tier might use 1024 texture caps, one 1024 shadow, no expensive transmission, and a 1× pixel ratio. A high tier can load close-detail textures on demand. Ensure switching tiers disposes replaced resources.
Build a memory ledger#
Maintain categories with estimated and observed values:
- unique texture allocations by format and mip chain;
- vertex, index, skin, and morph buffers;
- animation and CPU-side source buffers;
- environment and lighting resources;
- shadows;
- main and post-processing render targets;
- decoder and loader transient peak;
- browser or engine overhead allowance;
- safety margin.
Tie every entry to an owner and lifecycle: page, scene, asset cache, renderer, or temporary job. A resource without an owner is likely to leak.
On unload, record counts before disposal, immediately after disposal requests, and after a later idle period. Repeatedly cycle through the heaviest route 10 times. A staircase pattern indicates retained resources.
Reduction priorities#
For texture-driven memory:
- cut dimensions according to projected use;
- use suitable GPU-compressed targets;
- remove unused alpha and duplicate images;
- stream high mips or close-detail variants when supported;
- avoid retaining source pixels after upload;
- share common maps across assets.
For geometry-driven memory:
- remove unused attributes and morph targets;
- simplify candidates against visual error;
- preserve compact component types;
- share and instance identical buffers;
- load only required LODs;
- reduce animation keys with tolerances.
For target-driven memory:
- lower framebuffer resolution or pixel ratio;
- reduce shadow dimensions and count;
- reuse temporary targets;
- disable unnecessary history buffers or post effects;
- avoid simultaneous old and new scenes during transitions.
Memory acceptance checklist#
- Encoded, CPU-decoded, GPU-resident, and transient bytes are distinguished.
- Texture estimates use dimensions, formats, faces, layers, and mips.
- Runtime transcode targets are logged on representative devices.
- Geometry totals include all attributes, indices, morphs, and skinning.
- Loaded component types are verified after importer conversion.
- Unique resources are counted by identity, not visible object count.
- Framebuffers, shadows, probes, and post targets are in the ledger.
- Peak loading and route-transition memory are captured.
- Repeated navigation does not create a staircase of retained resources.
- Context loss produces a safe fallback and tested restoration path.
- Device tiers have explicit feature and resource limits.
- The production envelope includes safety margin for browser and OS variation.
Sources and further reading#
- MDN: WebGL best practices
- MDN: handling WebGL context loss
- Khronos KTX specification
- Khronos glTF 2.0 specification: binary data
- Three.js documentation: how to dispose resources
GPU memory planning is resource accounting plus lifecycle control. Estimate allocations from renderable formats, include the renderer's own targets, measure the overlap during loading, and prove that resources leave when their owner does.