Exportación y render en servidor
Qué formatos alcanza cada modelo, por qué un SVG se puede generar en el servidor sin navegador y un lienzo no, y cómo el requisito de mandar un PNG por correo decide toda tu arquitectura.
Los formatos que alcanza cada uno
| Salida | Desde SVG | Desde lienzo |
|---|---|---|
| Fichero vectorial | Directo, es su formato | No existe |
| PNG o WebP | Rasterizando, eligiendo la resolución | toBlob, a la resolución del búfer |
| PDF vectorial | Sí, con texto seleccionable | Solo como imagen incrustada |
| Editable en herramienta de diseño | Sí, con las capas y los nombres | No |
| Datos de píxel | Rasterizando primero | getImageData, directo |
| Cadena en el HTML inicial | Sí, sin JavaScript | No |
La fila que más decisiones cambia es la última. Un gráfico SVG generado en el servidor viaja dentro del HTML: se ve antes de que cargue ningún script, se ve con JavaScript desactivado, aparece en una captura de un rastreador y no produce ningún salto de diseño porque sus dimensiones están en el marcado. Un gráfico en lienzo es un rectángulo vacío hasta que el script se descarga, se ejecuta y dibuja.
Para un panel interno detrás de un inicio de sesión, da igual. Para una página pública, un informe que se comparte por enlace o un correo, es la diferencia entre funcionar y no funcionar.
La conversión de SVG a PNG en el cliente ya se trata, con sus limitaciones, en serializar y descargar. Lo que interesa aquí es el otro lado.
SVG en el servidor sin navegador
Este es el argumento decisivo y el menos conocido: generar SVG no requiere un DOM. Es texto.
// Node, cero dependencias, cero navegadorfunction graficoLineas(datos, { ancho = 640, alto = 240, m = { arriba: 12, dcha: 12, abajo: 24, izda: 40 } } = {}) { const w = ancho - m.izda - m.dcha; const h = alto - m.arriba - m.abajo; const xs = datos.map(d => d.t); const ys = datos.map(d => d.v); const [x0, x1] = [Math.min(...xs), Math.max(...xs)]; const y1max = Math.max(...ys); const px = t => ((t - x0) / (x1 - x0)) * w; const py = v => h - (v / y1max) * h; const r1 = n => Math.round(n * 10) / 10; const d = datos.map((p, i) => `${i ? 'L' : 'M'}${r1(px(p.t))} ${r1(py(p.v))}`).join(''); const marcas = [0, 0.5, 1].map(f => { const v = f * y1max, y = r1(py(v)); return `<line x1="0" y1="${y}" x2="${w}" y2="${y}" stroke="#313244"/>` + `<text x="-8" y="${y}" dy="0.32em" text-anchor="end" ` + `font-size="11" fill="#a6adc8">${Math.round(v)}</text>`; }).join(''); return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ancho} ${alto}" ` + `width="${ancho}" height="${alto}" role="img" aria-label="Evolucion de la metrica">` + `<g transform="translate(${m.izda},${m.arriba})">${marcas}` + `<path d="${d}" fill="none" stroke="#89b4fa" stroke-width="2" ` + `stroke-linejoin="round" stroke-linecap="round"/></g></svg>`;}Eso corre en cualquier proceso de Node, en un worker de borde, en un trabajo por lotes o en una función serverless. Sin navegador, sin DOM simulado, sin dependencias, en microsegundos. Y el resultado se puede meter tal cual en un HTML, guardar en un fichero, o mandar a una herramienta de conversión.
El xmlns es obligatorio en cuanto el SVG sale del HTML, y las razones están en el artículo de serialización. Y fíjate en que las escalas y el generador de ruta son los mismos del nivel 27 y del 28: la capa de cálculo no sabe dónde se ejecuta, que es exactamente lo que prometía la arquitectura de especificación separada del render.
Un lienzo, en cambio, es una API del navegador. Fuera de él hay dos caminos y los dos son pesados:
Una implementación nativa del contexto 2D. Existen paquetes que exponen la misma API sobre una biblioteca gráfica nativa. Funcionan bien y traen binarios compilados por plataforma, lo que complica el despliegue en entornos con arquitecturas mixtas o sin compilador.
Un navegador sin interfaz. Levantar un navegador de verdad, cargar la página, dejar que dibuje y capturar. Es la ruta más fiel, porque el resultado es idéntico al del cliente, y cuesta cientos de megabytes de imagen de contenedor y del orden de segundos por gráfico.
La diferencia de coste entre generar una cadena y arrancar un navegador es de varios órdenes de magnitud, y esa es la razón por la que casi todos los servicios que generan miniaturas de gráficos por lotes lo hacen en SVG.
Cuando el requisito llega tarde
Los tres requisitos que llegan al final del proyecto y que fuerzan una ruta de renderizado fuera del navegador:
«Queremos que el informe semanal llegue por correo con los gráficos.» El correo no ejecuta JavaScript y su soporte de SVG es irregular, así que los gráficos tienen que ser PNG generados en el servidor. No hay alternativa.
«Queremos un PDF descargable con el panel entero.» Si el PDF tiene que ser vectorial y con texto seleccionable, la ruta es SVG. Si vale un PDF con imágenes, sirve cualquier cosa, y hay que decidir la resolución de antemano.
«Queremos que la vista previa del enlace muestre el gráfico.» Las tarjetas de vista previa de las redes y de los mensajeros piden una imagen por URL. Eso es un endpoint que devuelve un PNG, generado en el servidor, sin sesión.
Los tres tienen la misma forma: el gráfico tiene que poder dibujarse sin usuario, sin sesión y sin navegador. Si la única implementación que tienes es un lienzo con manejadores de ratón, ese requisito es un proyecto entero. Si tienes una función pura que produce una especificación y dos renderizadores, es un fin de semana.