Memoria y peso: dónde se cruzan las curvas
Comparar el coste real de raster y vector en disco, en red y en memoria de ejecución, y calcular el punto en el que uno deja de compensar frente al otro.
Las comparaciones de peso entre raster y vector casi siempre miden lo que no importa. El tamaño del fichero comprimido es la parte menos interesante del coste: lo que decide si una página va bien es la memoria de ejecución y el tiempo de rasterización, y ahí las dos curvas se cruzan en sitios que sorprenden. Esta lección pone números a las tres dimensiones del coste y da un criterio para decidir.
- Separar el coste en disco, en red y en memoria de ejecución de un recurso gráfico.
- Calcular la memoria en tiempo de ejecución de un raster y estimar la de un vector.
- Identificar el punto de cruce donde el vector deja de ser más barato que el raster.
- Diagnosticar un problema de memoria gráfica a partir de sus síntomas.
Tres costes que no se parecen
Un recurso gráfico cuesta en tres sitios y las cifras no se correlacionan.
En disco y en red cuenta el fichero comprimido. Un PNG usa compresión sin pérdida sobre los píxeles; un JPEG, AVIF o WebP usan compresión con pérdida y bajan uno o dos órdenes de magnitud; un SVG es texto y comprime extraordinariamente bien con gzip o Brotli, a menudo a un quinto de su tamaño.
En memoria de ejecución cuenta lo descomprimido. Y aquí desaparece toda la ventaja de la compresión: un JPEG de 80 kB de 3000 por 2000 píxeles ocupa 24 MB en cuanto se decodifica. La compresión ahorra ancho de banda, no memoria.
En tiempo de rasterización cuenta la complejidad. Un raster se pinta con una operación de copia cuyo coste es proporcional al área de destino y prácticamente independiente del contenido. Un vector se pinta con un algoritmo cuyo coste depende del número de segmentos y del área cubierta.
Esa tercera columna es la que casi nadie mide y la que provoca los problemas más difíciles de diagnosticar, porque es la única que se paga cada vez que algo cambia.
| Recurso | En red | En memoria | Al repintar |
|---|---|---|---|
| Icono SVG de 20 segmentos | 0,6 kB | insignificante | insignificante |
| Icono PNG 24×24 | 0,9 kB | 2,3 kB | copia trivial |
| Foto JPEG 3000×2000 | 80 kB | 24 MB | copia grande |
| Mapa SVG de 40.000 puntos | 12 kB | modesta | muy caro |
| Canvas 3840×2160 | no aplica | 33 MB | según lo que dibujes |
La fila del mapa SVG es la instructiva: es el recurso más ligero de la tabla en red, no ocupa apenas memoria, y es el que puede tumbar el hilo principal.
El punto de cruce
Para una misma imagen, ¿cuándo pesa menos el vector? La respuesta depende de la relación entre número de primitivas y número de píxeles.
El vector cuesta, muy aproximadamente, del orden de veinte a cuarenta bytes por segmento en su representación textual, bastante menos comprimido. El raster cuesta cuatro bytes por píxel sin comprimir y una fracción variable comprimido.
Un icono de 24 por 24 tiene 576 píxeles; su versión vectorial con 20 segmentos ocupa unos cientos de bytes. Empate en red, victoria del vector en flexibilidad. Un logotipo a pantalla completa tiene dos millones de píxeles y sigue teniendo 50 segmentos: victoria aplastante del vector. Una fotografía tiene seis millones de píxeles y necesitaría cientos de miles de regiones para aproximarse: victoria aplastante del raster.
El cruce, en términos operativos, está en la densidad de detalle. Cuando el número de primitivas necesarias se acerca al número de píxeles, el vector ha dejado de tener sentido, y eso ocurre mucho antes de lo que la gente cree en cuanto la imagen deja de ser geométrica.
Hay un segundo cruce, independiente y más práctico: el número de tamaños distintos. Un raster hay que servirlo en cada resolución que necesites; un vector, una vez. Si un recurso se muestra a tres tamaños y en dos densidades de pantalla, el raster necesita seis variantes y el vector una. Eso mueve el umbral a favor del vector para cualquier cosa que sea de interfaz.
Aquí está el fallo que arruina más aplicaciones de canvas y que las herramientas ocultan casi por completo. Un HTMLCanvasElement, un ImageBitmap y una imagen decodificada tienen un objeto pequeño en el heap de JavaScript y un búfer enorme fuera de él, gestionado por el navegador. Cuando haces un snapshot de memoria, ese objeto aparece pesando unas decenas de bytes. Un canvas de 4K que ocupa 33 MB reales figura como un puñado de bytes en el heap. La consecuencia es que un bucle que crea canvas de caché sin liberarlos, o que decodifica imágenes en cada actualización, puede consumir gigabytes sin que el gráfico de memoria del panel de JavaScript se mueva del sitio. El síntoma real es distinto y engaña: primero la pestaña se vuelve lenta, después el navegador degrada el canvas a software porque se ha quedado sin memoria de vídeo, y al final la pestaña muere sin ningún error. Hay dos defensas concretas. La primera es llamar a close() sobre cualquier ImageBitmap que dejes de usar, que es el único recurso gráfico de la plataforma con liberación explícita y casi nadie lo hace. La segunda es poner un techo duro a cualquier caché de canvas —un número máximo de entradas, con expulsión de la menos usada— en lugar de dejar que crezca. Y para diagnosticar, la herramienta no es el heap de JavaScript, es el gestor de tareas del propio navegador, que sí muestra la memoria de la GPU por pestaña.
Medir en lugar de suponer
Los números de esta lección son órdenes de magnitud; para un caso concreto hay que medir. Estas tres medidas cubren casi todo.
Memoria de un recurso raster. No hay API que te la dé directamente, pero se calcula: ancho por alto por cuatro. Para una imagen cargada, naturalWidth y naturalHeight dan las dimensiones reales.
function bytesDeImagen(img) {
return img.naturalWidth * img.naturalHeight * 4;
}
// Sumar todas las imagenes del documento
const total = [...document.images].reduce((a, i) => a + bytesDeImagen(i), 0);
console.log((total / 1048576).toFixed(1) + ' MB descomprimidos');
Complejidad de un SVG. El indicador más útil es el número de comandos de sus trazados, que se aproxima contando los caracteres de comando en los atributos d.
function complejidad(svg) {
let n = 0;
for (const p of svg.querySelectorAll('path')) {
n += (p.getAttribute('d') || '').match(/[MLHVCSQTAZ]/gi)?.length ?? 0;
}
return n + svg.querySelectorAll('rect, circle, ellipse, line, polyline, polygon').length;
}
Por encima de unos pocos miles, cualquier animación o cambio de tamaño va a doler.
Coste de repintado. La única medida honesta es cronometrar el repintado real con el reloj de alta resolución, promediando varias pasadas para que el ruido no domine.
function medirRepintado(dibujar, pasadas = 60) {
dibujar(); // calentar
const t0 = performance.now();
for (let i = 0; i < pasadas; i++) dibujar();
return (performance.now() - t0) / pasadas;
}
La regla que se sostiene
De todo lo anterior sale un criterio corto que aguanta bien en producción.
Para interfaz —iconos, logotipos, ilustraciones geométricas, gráficas con pocos elementos— usa vector, sin pensarlo. Pesa menos, se adapta a cualquier densidad, hereda el color y es accesible.
Para contenido fotográfico usa raster, con formatos modernos y en varias resoluciones servidas con srcset. Vectorizar una foto no es una optimización, es un error.
Para escenas densas generadas por código usa canvas, y guarda tú el modelo. Aquí el vector pierde por complejidad de rasterización y el raster estático no sirve porque el contenido cambia.
Y una advertencia transversal: el recurso más caro es el que se decodifica y se tira. Una imagen que cargas, dibujas una vez en un canvas y descartas ha costado su decodificación completa para nada. Si el resultado se va a reutilizar, guarda el búfer; si no se va a reutilizar, no lo cargues.