wandres.dev
RENDERING · Los overlays de depuración

Regiones de salto de disposición: ver el contenido que se mueve

El overlay que colorea lo que se desplaza sin que el usuario lo pida, las cinco causas que produce, y por qué el instante en que ocurre importa tanto como la magnitud.

⏱ 16 min

El contenido que se mueve solo es uno de esos problemas que todo el mundo sufre y casi nadie reporta, porque el usuario no lo interpreta como un fallo sino como una torpeza propia: pulsó donde no era, y no se le ocurre que el botón se movió cien milisegundos antes de su dedo. La segunda casilla del panel de renderizado colorea exactamente esas regiones, y verlas es la única forma de encontrar las que ocurren demasiado rápido para percibirlas conscientemente.

🎯 Al terminar esta lección sabrás
  • Activar el overlay de regiones desplazadas y leer lo que colorea.
  • Enumerar las cinco causas habituales y su arreglo específico.
  • Explicar por qué un salto tras una interacción no cuenta y qué implica eso.
  • Registrar los saltos desde el código con los nodos responsables.

Qué colorea y cómo se usa

Activada la casilla correspondiente, cada región que se desplaza se resalta con un color durante un instante. A diferencia del resaltado de repintado, aquí lo que se marca no es lo que se redibuja sino lo que cambia de posición respecto a su ubicación anterior.

El uso correcto tiene una condición previa que casi nadie cumple: hay que provocar las condiciones en las que los saltos ocurren, y esas condiciones son casi siempre red lenta. Con la red del despacho, las imágenes llegan tan rápido que el contenido ya está en su sitio cuando aparece la primera pintura, y no hay salto que ver. Con una red ralentizada, los mismos saltos son enormes y evidentes.

El procedimiento es por tanto: activar el overlay, activar la ralentización de red, recargar, y mirar. Y después repetir con un desplazamiento lento hasta el final de la página, porque muchos saltos ocurren al cargarse las imágenes perezosas.

💡
Tip

Los saltos más dañinos son los que ocurren cerca de un elemento interactivo, y la forma de encontrarlos es reproducir el comportamiento real de un usuario: cargar la página y, en cuanto aparezca algo pulsable, intentar pulsarlo. Si el elemento se mueve entre que decides pulsarlo y que lo pulsas, has reproducido exactamente la peor experiencia que produce este problema.

Las cinco causas

Medios sin dimensiones. Una imagen o un vídeo sin ancho y alto declarados ocupa cero hasta que llegan sus datos, momento en el que empuja todo lo que hay debajo. Es la causa más frecuente y la más fácil de arreglar: declarar los atributos de dimensión permite al navegador reservar el espacio con la proporción correcta antes de tener el fichero. La proporción también se puede fijar con aspect-ratio, que es la solución cuando el tamaño es responsivo.

Contenido insertado por encima. Un banner, un aviso de cookies, una barra de notificación que se añade al principio del documento después de que la página se haya pintado. Todo lo que hay debajo baja. Arreglo: reservar el espacio desde el principio, o insertarlo en una capa superpuesta que no participe del flujo.

Sustitución de fuentes. El texto se pinta con la fuente de reserva y se recompone cuando llega la web. Si las métricas de las dos fuentes difieren, los bloques de texto cambian de altura. Arreglo: alinear las métricas de la fuente de reserva con descriptores de ajuste, o elegir una estrategia de visualización que evite el destello.

/* Ajustar la fuente de reserva para que ocupe lo mismo que la definitiva */
@font-face {
  font-family: "Reserva ajustada";
  src: local("Arial");
  size-adjust: 105%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}

body {
  font-family: "Mi fuente", "Reserva ajustada", sans-serif;
}

Contenido dinámico sin espacio reservado. Un componente que muestra un esqueleto de una altura y contenido real de otra. El esqueleto solo evita el salto si tiene exactamente las dimensiones del contenido que sustituye, cosa que casi nunca se comprueba.

Animaciones de propiedades de disposición. Animar la altura de un desplegable mueve todo lo que hay debajo en cada fotograma. Técnicamente es un salto en cada uno, aunque los que siguen a una interacción del usuario reciben un trato distinto.

El matiz de la interacción

Un salto que ocurre en la ventana inmediatamente posterior a una interacción del usuario no cuenta para la métrica. La razón es sensata: si el usuario pulsa un desplegable y el contenido de abajo se mueve, eso no es contenido que salta, es la consecuencia esperada de lo que acaba de pedir.

Ese matiz tiene dos consecuencias prácticas que conviene conocer.

La primera es de diagnóstico: si estás midiendo y tocas la página, dejas de medir lo que querías. Una medición de saltos tiene que hacerse sin interactuar.

La segunda es más incómoda y hay que decirla: la exclusión es una regla de la métrica, no una garantía de que no moleste al usuario. Un cambio de disposición grande tras un clic puede seguir siendo desconcertante, y no aparecerá en ningún número. La métrica es una aproximación al daño, no el daño.

Registrar los saltos desde el código

El overlay muestra dónde ocurren; el observador dice cuánto valen y qué nodos se movieron, que es la información que hace falta para arreglarlos.

// Registro de saltos con los nodos responsables y el momento exacto
(() => {
  let total = 0;
  const eventos = [];

  new PerformanceObserver(lista => {
    for (const e of lista.getEntries()) {
      if (e.hadRecentInput) continue;
      total += e.value;
      const nodos = e.sources.map(s => ({
        nodo: s.node,
        de: s.previousRect.top + ',' + s.previousRect.left,
        a: s.currentRect.top + ',' + s.currentRect.left,
        desplazamientoY: Math.round(s.currentRect.top - s.previousRect.top)
      }));
      eventos.push({ ms: Math.round(e.startTime), valor: +e.value.toFixed(4), nodos });
      if (e.value > 0.05) {
        console.warn('Salto grande de', e.value.toFixed(4), 'en el ms', Math.round(e.startTime));
        console.table(nodos.map(({ nodo, ...r }) => r));
        nodos.forEach(n => console.log('  nodo:', n.nodo));
      }
    }
  }).observe({ type: 'layout-shift', buffered: true });

  window.saltos = () => {
    console.log('Acumulado:', total.toFixed(4), '|', eventos.length, 'saltos registrados');
    console.table(eventos.map(e => ({ ms: e.ms, valor: e.valor, nodos: e.nodos.length })));
    return eventos;
  };
  console.log('Registrando saltos. Ejecuta saltos() para el resumen.');
})();

La propiedad sources es lo valioso: da los nodos concretos con su rectángulo antes y después. Con eso, el arreglo deja de ser una búsqueda y pasa a ser una tarea sobre un elemento identificado.

Una nota sobre la agregación: el valor total no es la suma simple de todos los saltos, sino el peor grupo de saltos consecutivos dentro de una ventana temporal limitada. Esa definición existe para que una sesión larga no acumule indefinidamente un valor malo por saltos aislados y separados en el tiempo. Para depurar, lo que interesa es la lista individual; para reportar, la agregación oficial.

El salto que hace daño de verdad ocurre en los cien milisegundos anteriores a un clic, y ninguna métrica lo distingue

La métrica de saltos mide desplazamiento acumulado ponderado por superficie, y esa definición captura razonablemente bien la sensación general de que una página es inestable. Lo que no captura, y es lo que de verdad produce daño, es la coincidencia entre un salto y la intención del usuario. Piensa en dos páginas con el mismo valor exacto. En la primera, todo el desplazamiento ocurre en los primeros doscientos milisegundos, mientras el usuario todavía está enfocando la vista: nadie se entera, la experiencia es perfecta y el número es malo. En la segunda, el desplazamiento ocurre a los mil ochocientos milisegundos, justo después de que el contenido pareciera estable y el usuario ya hubiera decidido dónde pulsar: el botón se mueve, el dedo aterriza en el enlace de al lado, y el usuario acaba en una pantalla que no pidió, con la sensación de que la aplicación hace cosas raras. Mismo número, daño radicalmente distinto. Y hay un caso todavía peor que ninguna métrica registra: el salto que ocurre después de que el usuario haya empezado el gesto pero antes de que el navegador procese el clic. Ese ni siquiera aparece como salto, porque queda dentro de la ventana de exclusión posterior a la interacción, y sin embargo es la única versión del problema que produce una acción no deseada. La conclusión práctica es que el objetivo no debería ser un número bajo sino una regla más estricta: que nada se mueva después de la primera pintura, punto. Esa regla es más exigente que la métrica y es mucho más fácil de verificar, porque es binaria y se comprueba con este overlay en treinta segundos. Y tiene un corolario de diseño que resuelve la mayoría de los casos de una vez: cualquier contenido cuya llegada no puedas garantizar antes de la primera pintura tiene que tener su espacio reservado desde el HTML inicial, o no participar del flujo en absoluto. Reservar espacio o superponer. No hay una tercera opción que sea segura, y la costumbre de insertar cosas en el flujo cuando llegan es la causa raíz de las cinco categorías de esta lección.