.gltf frente a .glb: el contenedor binario por dentro
El layout exacto de un fichero GLB con su cabecera y sus chunks, por qué base64 dentro de un .gltf es la peor opción posible, y cuándo conviene cada variante.
Las tres variantes
.gltf con recursos externos. Un JSON legible más un .bin con la geometría más un fichero por textura. El uri de cada buffer e imagen apunta a un fichero vecino.
.gltf autocontenido. El mismo JSON, pero cada uri es un data URI: data:application/octet-stream;base64,AAAAgD8.... Un solo fichero, todo dentro.
.glb. Un contenedor binario que lleva el JSON y los binarios en chunks contiguos.
El layout de un GLB, byte a byte
Es un formato muy simple y merece la pena verlo porque explica sus propiedades.
Cabecera, 12 bytes: [0..3] magic uint32 0x46546C67, que es "glTF" en ASCII little-endian [4..7] version uint32 2 [8..11] length uint32 longitud total del fichero en bytesChunk 0, obligatorio, JSON: [12..15] chunkLength uint32 [16..19] chunkType uint32 0x4E4F534A, que es "JSON" [20..] chunkData el JSON en UTF-8, rellenado con espacios 0x20 hasta multiplo de 4Chunk 1, opcional, BIN: chunkLength uint32 chunkType uint32 0x004E4942, que es "BIN\0" chunkData los bytes, rellenados con ceros hasta multiplo de 4Tres detalles del diseño que importan:
El JSON va primero. Un cargador puede leer los primeros kilobytes, parsear la estructura completa y saber qué contiene el fichero antes de haber descargado la geometría. Eso permite, por ejemplo, decidir si merece la pena seguir descargando, o empezar a preparar materiales mientras llegan los buffers.
El relleno a múltiplos de cuatro. El JSON se rellena con espacios y el binario con ceros para que el chunk BIN empiece siempre en una dirección alineada a 4 bytes. Eso permite crear vistas tipadas directamente sobre el ArrayBuffer recibido, sin copiar. Un Float32Array sobre un offset no múltiplo de 4 lanza una excepción en JavaScript; el relleno lo garantiza.
Los buffers sin uri apuntan al chunk BIN. Dentro del JSON de un GLB, el buffer 0 no tiene uri: eso significa «soy el chunk binario de este contenedor».
Por qué base64 es la peor opción
La codificación base64 representa cada 3 bytes con 4 caracteres ASCII. La sobrecarga es exactamente un 33.3 %, más el prefijo del data URI.
Y la penalización no acaba en el tamaño:
Se decodifica en el hilo principal. El navegador tiene que convertir la cadena a ArrayBuffer con atob o equivalente, y esa operación es síncrona y bloqueante. Para 20 MB de geometría en base64, eso son cientos de milisegundos de congelación.
Duplica la memoria durante la carga. El JSON entero, con sus datos base64 dentro, vive como cadena en memoria mientras se decodifica. Una cadena JavaScript de 27 MB más los 20 MB decodificados son 47 MB de pico.
Se comprime mal. Es tentador pensar que gzip recupera el 33 %, y lo recupera solo en parte: base64 destruye la estructura de bytes que los compresores explotan. Un .bin con floats se comprime mejor que su versión en base64, incluso comparando resultados finales.
La comparación completa, para un modelo cuyo contenido binario son 10 MB:
| Variante | Tamaño en disco | Peticiones | Decodificación extra | Caché parcial |
|---|---|---|---|---|
.gltf + .bin + texturas |
10.0 MB | 1 + 1 + N | ninguna | sí |
.gltf con base64 |
13.4 MB | 1 | ~200 ms bloqueantes | no |
.glb |
10.0 MB | 1 | ninguna | no |
La conclusión es que base64 no gana nunca: .glb le iguala en número de peticiones y le gana en todo lo demás. Su única justificación es el caso muy concreto de incrustar un modelo diminuto dentro de otro fichero de texto.
gltf separado frente a glb
Entre las dos opciones sensatas, la decisión depende del patrón de reutilización.
.glb gana cuando el modelo es una unidad que se carga entera, cuando las texturas son suyas y no se comparten, y cuando quieres una sola petición y un solo artefacto que versionar. Es el caso mayoritario y por eso es el formato por defecto de casi todo.
.gltf separado gana cuando varios modelos comparten texturas, porque el navegador las cachea una vez y las reutiliza; cuando quieres poder sustituir una textura sin volver a subir la geometría; cuando necesitas cargar progresivamente y mostrar la malla antes que las texturas; o cuando estás depurando y quieres poder abrir el JSON y editarlo.
Ese último punto tiene más peso del que parece en producción. Con .gltf separado y HTTP/2, N peticiones pequeñas no son caras, y una textura de 2 MB compartida entre diez modelos se descarga una vez en lugar de diez. En un configurador de producto con muchas variantes, esa diferencia domina.
Convertir entre variantes
Con gltf-transform, que se instala con npm install --global @gltf-transform/cli:
# De .gltf separado a .glbgltf-transform copy modelo.gltf modelo.glb# De .glb a .gltf separado, con las texturas extraidas a ficherosgltf-transform copy modelo.glb salida/modelo.gltf# Ver que hay dentro, sin abrir nadagltf-transform inspect modelo.glbinspect imprime una tabla con las mallas, sus primitivas, sus vértices, los materiales, las texturas con su resolución y formato, las animaciones y las extensiones usadas. Es el primer comando que hay que ejecutar sobre cualquier modelo que te llegue, antes de decidir nada.