wandres.dev
ONTOLOGÍA · El mapa del gráfico 2D en la web

Dónde vive cada modelo en el pipeline del navegador

Seguir el recorrido de un píxel desde el DOM hasta la pantalla y ver en qué etapa exacta se inserta CSS, SVG y canvas, para entender qué coste paga cada uno.

⏱ 18 min

Las diferencias entre los tres modelos gráficos dejan de parecer arbitrarias en cuanto ves dónde se enchufa cada uno en el pipeline de renderizado. CSS y SVG entran por arriba, atraviesan estilo y layout y llegan a la pintura convertidos en una lista de operaciones. El canvas entra por abajo: se salta las tres primeras etapas y entrega un búfer ya rasterizado. Todo lo que cuesta y todo lo que ahorra se explica desde ahí.

🎯 Al terminar esta lección sabrás
  • Enumerar las etapas del pipeline de renderizado y decir qué produce cada una.
  • Situar CSS, SVG y canvas en la etapa exacta en la que participan.
  • Explicar por qué el canvas es una caja opaca para el motor de estilo y layout.
  • Deducir del pipeline qué operaciones son baratas y cuáles caras en cada modelo.

Las etapas y qué produce cada una

El navegador convierte documento en píxeles siguiendo siempre las mismas etapas, y cada una consume lo que produjo la anterior.

Estilo. Para cada elemento del DOM se resuelve el valor calculado de todas las propiedades CSS. La entrada es el DOM más las hojas de estilo; la salida es un valor por propiedad y por elemento.

Layout. Con esos valores se construye el árbol de cajas y se resuelve la geometría: qué tamaño tiene cada caja y en qué posición del documento cae. La salida es geometría, no color.

Pintura. Cada caja se convierte en una lista de operaciones de dibujo —rellena este rectángulo, traza este borde, dibuja este texto— en el orden de apilamiento correcto. Ojo: pintar no es rasterizar todavía; es grabar qué habría que rasterizar.

Rasterización. Esa lista de operaciones se ejecuta y produce mapas de bits, normalmente por capas y a menudo en la GPU.

Composición. Las capas rasterizadas se combinan en la imagen final aplicando transformaciones y opacidades de capa. Esta es la etapa que puede ejecutarse sin tocar el hilo principal, y por eso animar transform y opacity es barato.

Dónde entra cada modelo

CSS participa en todas las etapas. Una propiedad de fondo se resuelve en estilo, condiciona la caja en layout, se graba en pintura y se rasteriza. Por eso una animación de width es cara —invalida layout, pintura y rasterización— y una de transform es barata: solo toca composición.

SVG participa en casi todas, con una particularidad. Los elementos SVG están en el DOM y reciben estilo como cualquier otro elemento, pero no participan en el layout de flujo del documento: dentro del elemento raíz <svg> la posición no la decide el flujo sino los atributos de coordenadas y el sistema de coordenadas de usuario. El elemento <svg> externo sí es una caja normal que el layout coloca; su interior es un universo de coordenadas propio. La consecuencia práctica es que mutar el atributo cx de un círculo no dispara un reflow del documento entero, pero sí invalida la geometría y la pintura de ese subárbol SVG.

El canvas participa en una sola. Para el motor de estilo y layout, un <canvas> es un elemento reemplazado igual que una <img>: tiene un tamaño intrínseco, ocupa una caja, y su contenido es opaco. Ninguna regla CSS del documento puede afectar a lo que hay dibujado dentro, porque el contenido no es un árbol de elementos: es un mapa de bits que tú produjiste. Cuando llega la etapa de pintura, el navegador simplemente copia ese búfer en la posición que le tocó.

flowchart TB
dom[DOM y hojas de estilo] --> estilo[Estilo]
estilo --> layout[Layout]
layout --> pintura[Pintura como lista de operaciones]
pintura --> raster[Rasterizacion]
raster --> comp[Composicion y pantalla]
css[CSS] -.entra aqui.-> estilo
svg[SVG] -.entra aqui.-> estilo
canvas[Canvas 2D] -.entra aqui.-> raster
style dom fill:#cba6f7,color:#11111b
style estilo fill:#89b4fa,color:#11111b
style layout fill:#89b4fa,color:#11111b
style pintura fill:#89b4fa,color:#11111b
style raster fill:#fab387,color:#11111b
style comp fill:#a6e3a1,color:#11111b
style css fill:#94e2d5,color:#11111b
style svg fill:#94e2d5,color:#11111b
style canvas fill:#f9e2af,color:#11111b

Qué se deduce del diagrama

Del sitio por donde entra cada modelo salen consecuencias que después parecen reglas sueltas y no lo son.

El CSS del documento no puede estilar el interior de un canvas. No es una restricción de seguridad ni un olvido: es que no hay nada que estilar. No hay elementos, no hay valores calculados, no hay cascada. Si quieres que el canvas respete el tema oscuro tienes que leer tú el valor y usarlo al dibujar.

// Leer un token del tema y usarlo dentro del canvas
const raiz = getComputedStyle(document.documentElement);
ctx.fillStyle = raiz.getPropertyValue('--acento').trim() || '#89b4fa';
ctx.fillRect(10, 10, 100, 40);

El canvas no dispara layout al dibujar. Puedes ejecutar diez mil operaciones de dibujo sin que el navegador recalcule ni un estilo. Ese es el ahorro real. Lo que sí cuesta es cambiar el tamaño del búfer, porque eso sí cambia el tamaño intrínseco del elemento reemplazado y puede afectar al layout del documento.

El zoom del navegador se comporta distinto en cada modelo. Al hacer zoom, CSS y SVG se rasterizan de nuevo a la nueva escala y salen nítidos, porque el navegador conserva la descripción vectorial. El canvas no: su búfer tiene el tamaño que le diste y al ampliarlo se interpola. Eso es lo que produce el canvas borroso, y el remedio pasa por detectar el cambio de escala y reconstruir el búfer.

Las herramientas de desarrollo son ciegas dentro del canvas. El inspector muestra el árbol del DOM. Dentro de un canvas no hay árbol, así que no hay nada que inspeccionar más allá del elemento en sí. Depurar canvas es depurar tu código, no el documento.

El canvas no está fuera del pipeline: está enchufado por el sitio más caro

Es tentador pensar que el canvas “se salta el navegador” y por eso es rápido. Se salta estilo y layout, sí, pero entra justo en la etapa que más se beneficia de que el navegador tenga información: la rasterización. Y ahí paga una factura que nadie enseña. Los navegadores modernos aceleran el canvas 2D en GPU cuando pueden, lo que significa que tus llamadas de dibujo se encolan como comandos y se ejecutan de forma asíncrona. Todo va bien mientras el flujo sea en un solo sentido —CPU manda comandos, GPU dibuja—. En cuanto pides datos de vuelta con getImageData, obligas a vaciar la cola, esperar a que la GPU termine y copiar memoria de vídeo a memoria del sistema. Ese viaje de vuelta es el pico de latencia más brutal que puede tener un canvas, y es la razón de que exista una opción del contexto dedicada a avisar de que vas a hacerlo a menudo. El canvas no es rápido por saltarse el pipeline: es rápido mientras respetes la dirección en la que fluye.

Un experimento que lo enseña

La forma más directa de ver la diferencia de coste es medir dónde se va el tiempo en cada modelo con la misma carga. Este fragmento crea mil nodos SVG y los mueve, y hace lo equivalente en canvas; ejecútalo con el panel de rendimiento abierto y compara qué barras aparecen.

<div id="a"></div>
<canvas id="b" width="600" height="400"></canvas>
<script>
  const N = 1000;
  const puntos = Array.from({ length: N }, () => ({
    x: Math.random() * 600, y: Math.random() * 400,
    vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2,
  }));

  // Version SVG: N nodos, N mutaciones de atributo por fotograma
  const ns = 'http://www.w3.org/2000/svg';
  const svg = document.createElementNS(ns, 'svg');
  svg.setAttribute('width', 600); svg.setAttribute('height', 400);
  const nodos = puntos.map(() => {
    const c = document.createElementNS(ns, 'circle');
    c.setAttribute('r', 2); c.setAttribute('fill', '#a6e3a1');
    svg.appendChild(c); return c;
  });
  document.getElementById('a').appendChild(svg);

  // Version canvas: 1 elemento, N llamadas de dibujo por fotograma
  const ctx = document.getElementById('b').getContext('2d');

  function paso() {
    for (const p of puntos) {
      p.x += p.vx; p.y += p.vy;
      if (p.x < 0 || p.x > 600) p.vx *= -1;
      if (p.y < 0 || p.y > 400) p.vy *= -1;
    }
    nodos.forEach((n, i) => {
      n.setAttribute('cx', puntos[i].x.toFixed(1));
      n.setAttribute('cy', puntos[i].y.toFixed(1));
    });
    ctx.clearRect(0, 0, 600, 400);
    ctx.fillStyle = '#89b4fa';
    for (const p of puntos) {
      ctx.beginPath();
      ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
      ctx.fill();
    }
    requestAnimationFrame(paso);
  }
  paso();
</script>

En el perfil verás que el trabajo del SVG aparece repartido entre estilo, layout y pintura, mientras que el del canvas es una única barra de ejecución de script seguida de rasterización. No es que uno sea “mejor”: es que el trabajo cae en sitios distintos, y saber en cuál cae es lo que te permite optimizarlo.