Texture Size vs. Mesh Size: Finding the Real 3D Bottleneck
Compare textures and mesh data across transfer, decode, GPU memory, bandwidth, and frame time to choose the right optimization first.
Textures and meshes consume different resources at different times. A texture can be tiny as a JPEG, large when decoded, and efficient or expensive when sampled on the GPU. A mesh can compress to a small network payload, expand into several vertex buffers, and cost additional work every time its vertices are transformed. Asking which one is “bigger” needs a stage and a metric.
Use four budgets: transfer bytes, peak load memory, resident GPU memory, and frame time. The dominant side can change from one budget to the next.
Start with two independent inventories#
For every texture, record:
- width and height;
- channel count and semantic role;
- encoded format and bytes;
- expected GPU format;
- mipmap policy;
- color-space interpretation;
- material references and UV set;
- closest projected screen coverage.
For every mesh primitive, record:
- triangles and rendered vertices;
- index component type;
- vertex attributes and component types;
- raw and compressed buffer bytes;
- morph-target attributes;
- skinning influences;
- material assignment;
- visible instance count in representative scenes.
Do not collapse the rows into one GLB total until the details are retained elsewhere. The total tells you whether a limit was missed; the rows tell you how to fix it.
Transfer bytes favor content-dependent compression#
Encoded image size depends heavily on content. A 2048 × 2048 flat-color image can compress far smaller than a noisy 1024 × 1024 scan. JPEG quality, PNG filtering, WebP settings, and KTX2 mode also change the payload. Dimensions alone do not predict network bytes.
Geometry compression likewise responds to regularity, quantization, ordering, and attributes. A smooth, indexed manufactured part may compress efficiently. Photogrammetry positions and normals containing high-frequency noise carry more entropy. Two 500,000-triangle meshes can have very different encoded sizes.
This means a storage comparison should use actual candidates. Re-encode a color texture at controlled quality. Quantize and compress the mesh at a tested precision. Record cold-cache decoder dependencies. Compare the complete response bytes, not screenshots from separate tools with different defaults.
As a simple example, a GLB may contain 2.2 MB of compressed geometry and 6.8 MB of images. A geometry pass that saves 55% removes about 1.21 MB. An image pass that saves 40% removes about 2.72 MB. Neither percentage alone reveals which change moves the 9 MB total further.
Decoded image memory follows texels#
Once decoded to a conventional RGBA8 surface, image memory is approximately width × height × 4 bytes. A 2048 × 2048 texture uses 16 MiB at its top level. A 4096 × 4096 texture uses 64 MiB. Adding a complete mip chain increases the amount by roughly one third, giving approximately 21.3 MiB and 85.3 MiB respectively.
Six 2048 RGBA8 textures with mips therefore approach 128 MiB. Six equivalent 4096 textures approach 512 MiB. Their PNG or JPEG files might total less than 30 MB, so network panels alone can drastically understate memory pressure.
GPU-native block compression changes resident size. A format using 8 bits per pixel needs approximately one quarter of RGBA8's top-level bytes; a 4-bits-per-pixel format needs about one eighth. Actual transcode target, block dimensions, alpha support, and quality vary by device and texture type. Query or log the selected runtime format instead of assuming every KTX2 becomes the same allocation.
Peak memory can exceed the final texture total while encoded bytes, decoded pixels, transcode intermediates, and uploaded images coexist. Load tests should capture the peak on the lowest-memory supported device.
Mesh memory follows attributes and duplication#
A mesh's resident buffer memory is more predictable from its final layout. For 200,000 rendered vertices using 48 bytes of position, normal, tangent, and UV data, vertex buffers require 9.6 MB decimal. If 350,000 triangles use 32-bit indices, their 1,050,000 indices require 4.2 MB. The combined raw geometry is about 13.8 MB.
Add four float position morph targets and the vertex payload grows by 4 × 200,000 × 12 = 9.6 MB. Add normal morph deltas and another 9.6 MB appears. Texture-heavy static props and morph-heavy characters therefore need different optimization priorities.
Seams influence these numbers. A source may show 120,000 positions but export 200,000 vertices because normal, tangent, UV, material, and skin attributes differ at shared locations. Welding by position would corrupt those properties. Classify splits before trying to remove them.
Compressed geometry still expands to a renderable form. Draco transfer bytes are not the vertex-buffer allocation. Quantized attributes may remain compact in GPU buffers when the loader and shader path support them, but importer conversion can also expand data. Inspect actual buffer attributes after loading.
Frame cost is not proportional to memory#
A resident 64 MiB texture does not transfer all 64 MiB through the shader for every fragment. Texture caches, mip selection, filtering, screen coverage, compression, and access pattern influence bandwidth. A large texture on a tiny distant object may sample a small low-resolution mip; it still occupies memory unless streamed, but its immediate sampling cost can be modest.
A mesh with 2 million triangles does not necessarily render 2 million visible triangles after object culling, though a single large primitive limits fine culling. It can be drawn in shadow, depth, reflection, and color passes, multiplying work. Small or back-facing triangles may be rejected later in the pipeline after some vertex work has already occurred.
Texture optimization mainly affects:
- acquisition and image decode;
- resident memory;
- texture sampling bandwidth and cache behavior;
- fragment shading quality and cost;
- overdraw when alpha changes render behavior.
Mesh optimization mainly affects:
- acquisition and geometry decode;
- vertex and index buffer memory;
- skinning or morph processing;
- vertex-shader work across passes;
- rasterization efficiency and culling granularity.
Material and primitive structure connects the two. An atlas might reduce material submissions but increase texture area. Splitting a mesh can improve culling while increasing draw calls.
Diagnose memory pressure#
Signs of a texture-driven memory problem include:
- crashes or tab reloads during image decode;
- large growth when texture upload completes;
- blurry or missing textures after driver pressure;
- failures that correlate with image dimensions rather than triangle count;
- major improvement after a one-half dimension test.
Signs of geometry-driven memory include:
- many attributes or morph targets in profiler captures;
- numerous unique mesh copies instead of shared buffers;
- large array buffers retained after parsing;
- vertex counts far above source positions;
- memory growth proportional to loaded LODs or animation variants.
Create two diagnostic builds. In one, replace textures with 4 × 4 placeholders while retaining all materials and geometry. In the other, render a heavily reduced mesh with original textures. Compare load peak, steady memory, and frame behavior. These are diagnostic controls, not shipping candidates.
Diagnose frame-time pressure#
To test whether vertex work is limiting, reduce geometry aggressively or render a lower LOD while preserving screen coverage and material. If GPU time falls substantially, investigate vertices, passes, skinning, and triangle efficiency. If it does not, the scene may be fragment-, CPU-, or synchronization-bound.
To test texture and fragment effects, reduce render resolution, replace materials with a cheap opaque shader, or use small neutral textures. A strong resolution response suggests fragment cost. A strong material response with little geometry response points toward shader, texture, transparency, or overdraw work.
Change one dimension at a time:
- original asset and material;
- original mesh with cheap material;
- reduced mesh with original material;
- reduced mesh with cheap material;
- original scene at 50% pixel resolution;
- original scene with shadows disabled.
Record CPU and GPU frame timing for each. The pattern is more useful than a single “fps” number.
Screen-space texture requirements#
Texture dimensions should follow how many unique texels can become visible. If a product occupies 800 pixels and one UV island covers a quarter of its visible height, that region needs around 200 displayed samples before filtering and perspective considerations. Allocating 2048 texels along the same island direction may be unnecessary unless deeper zoom is allowed.
Use a texture-density or mip-level visualization in the target runtime. It reveals when the top mip is never sampled. If a 4096 image always displays mip 2 or smaller, a 1024 base may preserve the normal experience and remove 93.75% of the texels compared with 4096.
Do not downsize every map equally. A broad roughness variation may survive at 512 while fine base-color text needs 2048. Packing those channels into one image couples their dimensions. Channel packing saves files and bindings but can force low-information data to share the largest required resolution.
Normal maps need vector-aware filtering and enough resolution to preserve their intended frequency. Alpha masks may need coverage-preserving mip generation. Review moving cameras, because shimmer and mip transitions are temporal defects.
Screen-space geometry requirements#
Geometry should similarly follow projected curvature and silhouette. Render a wireframe at the closest allowed view. If triangles are consistently much smaller than a pixel across flat areas, there is likely reducible density. If a round edge shows visible straight segments, it needs more geometric samples or a restricted camera.
Geometry below one pixel is not always waste. It can affect shadows, occlusion, collisions, skin deformation, or a later close view. Classify those roles. A hidden internal gear in a non-transparent appliance has different value from a thin antenna that controls a recognizable outline.
Generate a reduction ladder and compare screen-space error. A 50% triangle candidate can preserve nearly every visible pixel on one model and destroy a sparse signature feature on another.
Reuse changes the equation#
Ten instances can share one geometry and one texture set when the runtime references identical resources. Memory stays close to one asset plus per-instance transforms. Ten separately exported copies with distinct URLs or modified material objects can defeat sharing.
Textures often offer strong reuse through tiling materials, trim sheets, or common product components. Mesh geometry offers reuse through instancing. Measure unique GPU resources, not just visible object count.
Atlasing can reduce state changes but may prevent independent loading or increase unique pixels. Instancing requires the same geometry and compatible material path. Reuse is valuable only when identity and lifecycle are preserved through application code.
Test navigation. If leaving and returning to a page creates another 85 MiB texture allocation because URLs or loader caches differ, asset optimization alone will not solve the leak.
A prioritization method#
Score opportunities against the product's failing budget:
- transferred bytes saved;
- peak memory saved;
- resident memory saved;
- milliseconds of decode saved;
- milliseconds of frame time saved;
- visible quality risk;
- engineering and art effort;
- compatibility risk.
For a commerce page failing first interaction, a 3 MB base-color saving may outrank a small GPU improvement. For an installed mobile game crashing after 20 minutes, resident memory and sustained frame time matter more than cold network bytes. For WebXR, frame stability and thermal behavior usually dominate once placement starts.
Choose the highest-impact low-risk experiment, build it, and replace estimates with measurements.
Comparison checklist#
- Texture and geometry totals are reported separately.
- Image dimensions and GPU formats accompany encoded sizes.
- Mip memory is included in resident estimates.
- Geometry bytes include indices, attributes, morphs, and skin data.
- Loaded runtime attributes are checked for silent expansion.
- Peak load memory is captured on a low-memory target.
- Frame tests isolate vertex, fragment, material, and resolution effects.
- Texture resolution follows closest projected use.
- Triangle density follows silhouette, curvature, deformation, and pass needs.
- Repeated assets share actual GPU resources.
- The optimization targets the budget the product is missing.
- Quality comparisons use fixed cameras, lighting, and output resolution.
Sources and further reading#
- Khronos glTF 2.0 specification: textures
- Khronos glTF 2.0 specification: binary data storage
- Khronos KTX specification
- NVIDIA GPU Gems: graphics pipeline performance
- MDN: WebGL best practices
The useful answer to texture size versus mesh size is a profile, not a slogan. Compare both through transfer, allocation, and rendering, then spend quality where it improves the actual failing budget.