wandres.dev
CORE WEB VITALS · LCP, INP y CLS

CLS con precisión: la ventana de sesión y la puntuación

La ventana de sesión de cinco segundos, la fórmula de fracción de impacto por fracción de distancia, qué desplazamientos se excluyen y por qué la definición cambió.

⏱ 18 min

Mucha gente cree saber qué es el CLS y en realidad conoce la versión de 2020, que ya no es la vigente. La métrica dejó de ser una suma acumulada durante toda la vida de la página para pasar a ser la mayor ráfaga dentro de una ventana de sesión con dos límites simultáneos. Esa ventana, y la fórmula de puntuación que hay debajo, son la diferencia entre entender la métrica y repetirla.

🎯 Al terminar esta lección sabrás
  • Definir la ventana de sesión con sus dos condiciones y aplicarla a una secuencia de desplazamientos.
  • Calcular la puntuación de un desplazamiento a partir de la fracción de impacto y la de distancia.
  • Enumerar qué desplazamientos no cuentan y bajo qué condiciones exactas.
  • Explicar qué problema resolvió el cambio de definición.

Qué es un desplazamiento de layout

Un desplazamiento de layout ocurre cuando un elemento visible cambia su posición de inicio de un fotograma renderizado al siguiente. La posición de inicio es, en el modo de escritura habitual, su posición superior e izquierda. A esos elementos se les llama elementos inestables.

La definición tiene una consecuencia que sorprende y que hay que tener muy clara: si un elemento nuevo se añade al DOM, o si un elemento existente cambia de tamaño, eso no es un desplazamiento de layout por sí mismo. Solo lo es si provoca que otros elementos visibles cambien su posición de inicio.

Un ejemplo del propio material de referencia lo ilustra bien. Una caja gris crece de tamaño y se le añade un botón dentro. La caja gris no es un elemento inestable porque su posición de inicio no ha cambiado, solo su altura. El botón tampoco lo es porque antes no estaba en el DOM. Pero la caja verde que había debajo sí se desplaza hacia abajo, y esa sí es inestable.

La fórmula de la puntuación

La puntuación de un desplazamiento individual es el producto de dos fracciones:

puntuacion = fraccion de impacto x fraccion de distancia

La fracción de impacto mide cuánta superficie de la ventana se ve afectada. Es la unión del área visible de todos los elementos inestables en el fotograma anterior y en el actual, dividida por el área total de la ventana. Si un elemento ocupa la mitad de la ventana y baja un 25% de la altura, la unión de sus dos posiciones cubre el 75% de la ventana, así que la fracción de impacto es 0,75.

La fracción de distancia mide cuánto se ha movido. Es la mayor distancia horizontal o vertical que haya recorrido cualquier elemento inestable, dividida por la mayor dimensión de la ventana, es decir, su anchura o su altura, la que sea mayor. En el ejemplo anterior, si la altura es la dimensión mayor y el elemento baja un 25% de ella, la fracción de distancia es 0,25.

La puntuación de ese desplazamiento sería 0,75 x 0,25 = 0,1875.

La fracción de distancia no estaba en el diseño original: la puntuación se calculaba solo con la fracción de impacto. Se añadió porque penalizaba desproporcionadamente los casos en que un elemento muy grande se movía muy poco, que visualmente son inofensivos.

Un segundo ejemplo con varios elementos aclara la unión. Una lista alfabética recibe entradas nuevas y las que ya estaban se recolocan. Los elementos nuevos no cuentan, porque no estaban en el DOM. El primero de la lista tampoco, porque no se movió. Los tres que sí se recolocaron son los inestables, la unión de sus áreas antes y después cubre alrededor del 60% de la ventana, y el que más se movió bajó un 30% de la altura. Puntuación: 0,60 x 0,3 = 0,18.

La ventana de sesión

Y ahora la parte que da nombre a esta lección. El CLS de una página no es la suma de todas esas puntuaciones. Es la puntuación de la mayor ráfaga.

Una ráfaga, o ventana de sesión, es un conjunto de desplazamientos que cumplen dos condiciones a la vez:

  1. Entre un desplazamiento y el siguiente pasa menos de 1 segundo.
  2. La ventana completa dura como máximo 5 segundos.

Cuando se rompe cualquiera de las dos condiciones, la ventana se cierra y empieza una nueva. El CLS de la página es la ventana con la suma acumulada más alta de todas las que se hayan formado.

Vale la pena seguir un ejemplo paso a paso:

t=0,2 s  desplazamiento 0,04   ventana A abierta   acumulado A = 0,04
t=0,9 s  desplazamiento 0,03   +0,7 s desde el anterior, sigue en A   A = 0,07
t=1,5 s  desplazamiento 0,05   +0,6 s, sigue en A                     A = 0,12
t=3,0 s  desplazamiento 0,02   +1,5 s, mas de 1 s -> cierra A, abre B  B = 0,02
t=3,4 s  desplazamiento 0,06   +0,4 s, sigue en B                      B = 0,08
t=9,0 s  desplazamiento 0,09   +5,6 s -> cierra B, abre C              C = 0,09

CLS = max(0,12  0,08  0,09) = 0,12

La condición de los 5 segundos entra en juego cuando hay desplazamientos continuos y frecuentes: aunque nunca pase un segundo entero sin actividad, la ventana se cierra a los 5 segundos de haberse abierto y arranca otra. Sin ese tope, una página con un carrusel que se mueve cada 800 milisegundos acumularía indefinidamente.

⚠️
La versión antigua sigue apareciendo en herramientas y en artículos

Hasta 2021, CLS era la suma de todos los desplazamientos durante toda la vida de la página. Esa definición castigaba brutalmente a las páginas de sesión larga: una aplicación abierta durante horas acumulaba puntuación aunque cada desplazamiento fuese despreciable. Si lees un artículo o usas una herramienta que describe CLS como una suma total, está desactualizada. La documentación oficial mantiene un listado de qué herramientas permiten todavía comparar con la implementación original.

Qué desplazamientos no cuentan

Cuatro exclusiones, con sus condiciones exactas.

Los desplazamientos iniciados por el usuario, con matices. Los desplazamientos que ocurren dentro de los 500 milisegundos siguientes a una entrada del usuario llevan activado el indicador hadRecentInput y se pueden excluir del cálculo. Pero ojo, y este matiz se ignora constantemente: ese indicador solo se activa para eventos de entrada discretos, es decir, toque, clic y pulsación de tecla. Las interacciones continuas, como el desplazamiento de la página, el arrastre o el gesto de pellizco, no se consideran entrada reciente. Si tu contenido salta mientras el usuario hace scroll, eso suma al CLS.

Las animaciones y transiciones con transform. Un elemento animado con la propiedad transform no cambia su posición de inicio en el layout, así que no genera desplazamientos. Esta es la razón técnica de la recomendación clásica: en lugar de animar top, right, bottom o left, usa transform: translate(); en lugar de animar height y width, usa transform: scale().

Las páginas cargadas en segundo plano. Si la página se carga en segundo plano, o pasa a segundo plano antes de que el navegador pinte nada, no debe reportar valor de CLS.

La restauración desde la caché de retroceso y avance. Cuando una página se restaura desde esa caché, su CLS se reinicia a cero, porque el usuario lo vive como una visita distinta.

Como en las otras dos métricas, hay una divergencia entre la API y la métrica en el asunto de los iframes: la API no reporta desplazamientos ocurridos dentro de un iframe, pero la métrica sí los cuenta, porque forman parte de la experiencia. La librería web-vitals documenta esto explícitamente como su única limitación no cubierta, y por eso lo que mide técnicamente en presencia de iframes es el CLS del documento, no el de la página completa.

Medirlo en el navegador

Observar los desplazamientos en crudo es la manera más rápida de encontrar el culpable durante el desarrollo.

// Registra cada desplazamiento con su puntuacion y los elementos implicados.
new PerformanceObserver((lista) => {
  for (const entrada of lista.getEntries()) {
    if (entrada.hadRecentInput) continue;    // entrada discreta reciente
    console.log(
      'desplazamiento', entrada.value.toFixed(4),
      'a', Math.round(entrada.startTime), 'ms',
      entrada.sources.map((s) => s.node),
    );
  }
}).observe({ type: 'layout-shift', buffered: true });

La propiedad sources contiene los elementos que se movieron, con sus rectángulos anterior y posterior. Es la información que necesitas para localizar el culpable, y es justo lo que expone el build de atribución de la librería en largestShiftTarget.

Para el valor agregado, la implementación correcta es la de la librería, que ya resuelve la ventana de sesión, la restauración desde la caché de retroceso y avance, y el reporte al pasar a segundo plano.

El CLS que te hunde ocurre a los siete segundos, no durante la carga

La intuición es que el CLS es un problema de carga y que se arregla declarando las dimensiones de las imágenes. Esa es la mitad fácil. La mitad que hunde a los sitios reales ocurre mucho después: el aviso de consentimiento que aparece cuando el usuario ya está leyendo, el anuncio que se recarga y cambia de altura, la barra de notificación que se inyecta al recibir un evento del servidor, el contenido de scroll infinito que empuja el pie de página. Todo eso está fuera de la ventana de carga, y por tanto no aparece en ninguna herramienta de laboratorio, que carga la página y se va. La forma de encontrarlo es guardar, junto al valor de CLS, el instante largestShiftTime y el elemento largestShiftTarget, y hacer un histograma de ese instante. Si tu masa de desplazamientos está por debajo del segundo, tienes un problema de carga y las dimensiones lo arreglan. Si tienes una segunda joroba entre los cinco y los quince segundos, tienes un problema de contenido inyectado, y ninguna cantidad de aspect-ratio va a tocarlo.