wandres.dev
RENDERIZADO III · contain y content-visibility

contain-intrinsic-size, la barra de scroll y la búsqueda en la página

Cómo estimar el tamaño de lo que el navegador no ha renderizado, qué hace la palabra clave auto, y qué le pasa a la barra de scroll, a Ctrl+F y a las anclas cuando la estimación falla.

⏱ 18 min

En cuanto le dices al navegador que no renderice un subárbol, aparece una pregunta sin respuesta: ¿cuánto ocupa? La altura del documento es la suma de las alturas de todo, y si mil secciones no se han maquetado, mil sumandos son desconocidos. La respuesta que des determina la longitud de la barra de scroll, la posición de las marcas de búsqueda, si un ancla cae donde debe y si la página tiembla al desplazarse. Es la parte de content-visibility que separa una mejora de un desastre de usabilidad.

🎯 Al terminar esta lección sabrás
  • Escribir estimaciones correctas con contain-intrinsic-size y sus formas de un eje.
  • Explicar qué hace la palabra clave auto y por qué convierte una estimación fija en una autocorrectiva.
  • Predecir el comportamiento de la barra de scroll según el error de la estimación y su signo.
  • Verificar que la búsqueda dentro de la página, las anclas y el árbol de accesibilidad siguen funcionando.

El tamaño que el navegador no puede saber

Cuando un elemento tiene contención de tamaño —porque le pusiste contain: size o porque content-visibility: auto la activó al salirse de pantalla— el navegador lo dimensiona como si estuviera vacío. En flujo normal, un bloque vacío mide cero de alto. Mil secciones saltadas miden cero. El documento entero mide lo que miden las secciones visibles.

contain-intrinsic-size es la propiedad que rellena ese hueco. Su valor es el tamaño que el navegador debe suponer mientras no tenga el real.

.seccion {
  content-visibility: auto;
  contain-intrinsic-size: auto 900px;
}

La sintaxis admite uno o dos valores, y cada uno puede ir precedido de auto. Con dos valores, el primero es el eje en línea y el segundo el eje de bloque. Existen además las formas de un solo eje, que suelen ser más claras:

/* Equivalentes en flujo normal, y mas explicito el segundo. */
.a { contain-intrinsic-size: auto 900px; }
.b { contain-intrinsic-block-size: auto 900px; }

Para un elemento de bloque en flujo normal el eje en línea da igual: con width: auto la caja rellena su contenedor y la contención de tamaño no cambia eso, porque el ancho no se estaba resolviendo por contenido de todas formas. Donde sí importa el eje en línea es en elementos de una rejilla o de un contenedor flexible dimensionados por contenido, donde una estimación de cero en ese eje colapsa la columna.

La palabra clave auto y la memoria del último tamaño

Escribir un número fijo es una apuesta: aciertas en las secciones de longitud media y fallas en las cortas y en las largas. La palabra clave auto convierte la apuesta en un valor que se corrige solo.

contain-intrinsic-size: auto 900px significa: usa 900 píxeles hasta que hayas renderizado este elemento al menos una vez; a partir de entonces, usa el tamaño real que mediste la última vez. El navegador lo llama last remembered size y lo conserva mientras el elemento siga en el documento.

La consecuencia práctica es grande y merece detenerse. En un scroll hacia abajo por una página larga, la primera pasada usa la estimación y la barra se comporta como se comporte. Pero en cuanto el usuario ha pasado por una sección, esa sección ya conoce su altura verdadera, así que el scroll hacia arriba es exacto. Y como el patrón de uso real de una página larga es bajar y volver a subir muchas veces, el usuario percibe la imprecisión una sola vez y la precisión el resto de la sesión.

Sin auto, la estimación se usa siempre, incluso para secciones ya vistas, y los saltos se repiten en cada pasada. Salvo que tengas el tamaño exacto —por ejemplo porque tus filas son todas de 48 píxeles— usa siempre auto. No tiene coste y elimina la mitad del problema.

Para elegir el número que acompaña a auto, la estimación honesta se saca del contenido real, no de la intuición:

// Ejecutalo una vez sobre una pagina representativa sin content-visibility.
function estadisticasDeAltura(selector) {
  const alturas = Array.from(document.querySelectorAll(selector))
    .map((el) => el.getBoundingClientRect().height)
    .sort((a, b) => a - b);
  if (!alturas.length) return null;
  const p = (q) => Math.round(alturas[Math.min(alturas.length - 1,
                                               Math.floor(alturas.length * q))]);
  return {
    n: alturas.length,
    min: Math.round(alturas[0]),
    p50: p(0.5),
    p75: p(0.75),
    p90: p(0.9),
    max: Math.round(alturas[alturas.length - 1]),
  };
}

console.table(estadisticasDeAltura('.seccion'));

Con esos números, la elección tiene una lógica que se explica en el apartado siguiente: conviene equivocarse por arriba, no por abajo, así que la mediana no es la mejor elección y la p75 suele serlo.

Y si tu contenido lo permite, hay una opción mejor que estimar: calcular. Si el servidor sabe cuántos párrafos tiene cada sección, puede emitir el valor en línea, y la estimación deja de ser una constante global para pasar a ser un dato por elemento.

<section class="seccion" style="contain-intrinsic-block-size: auto 1240px">
  <!-- 1240 = altura estimada a partir del numero de caracteres y de la
       altura de linea, calculada en el servidor -->
</section>

La barra de scroll y el signo del error

La altura del documento es la suma de estimaciones y tamaños reales. Según hacia dónde falles, el usuario nota una cosa u otra.

Si estimas de menos, el documento parece más corto de lo que es. La barra de scroll nace larga y encoge a medida que el usuario baja y las secciones reales resultan ser más altas. El efecto es el clásico “cuanto más bajo, más queda”, y es el peor de los dos porque produce la sensación de que la página no termina nunca. Además el pulgar de la barra se mueve de forma no lineal.

Si estimas de más, el documento parece más largo. La barra nace corta y crece. Es menos molesto: el usuario percibe que avanza más rápido de lo esperado, que psicológicamente es al revés de frustrante.

En ambos casos hay un mecanismo del navegador que amortigua el daño: el anclaje de scroll. Cuando el contenido por encima del punto de vista cambia de tamaño, el navegador ajusta la posición de scroll para que lo que el usuario está mirando no se mueva. Funciona bien y está activo por defecto, y es la razón de que estas imprecisiones se noten en la barra pero no en el contenido. Puedes desactivarlo con overflow-anchor: none, y hay exactamente un motivo legítimo para hacerlo —un chat que debe quedarse pegado abajo— y ninguno más. Si alguna vez ves ese valor en un CSS ajeno, sospecha.

Hay un caso donde el error de estimación sí produce un salto visible: cuando la sección que se revela está por encima del punto de vista y su tamaño real difiere mucho del estimado justo en el momento en que el usuario mira. Ocurre sobre todo con estimaciones muy malas —un orden de magnitud— y se corrige acercando el número, no tocando el anclaje.

Toda virtualización es una mentira sobre tamaños, y lo único que puedes elegir es cuánto dura

Da igual la técnica: content-visibility, una lista virtualizada en JavaScript, la paginación por scroll infinito o el renderizado perezoso de un framework. En todas, el navegador tiene que dibujar una barra de scroll antes de conocer la altura del contenido, y por tanto en todas hay un instante en el que la interfaz afirma algo que no ha comprobado. La única diferencia entre las técnicas es quién mantiene la mentira y cuánto tarda en corregirse. En una lista virtualizada escrita a mano, la mentira es tuya: tú calculas la altura total como número de elementos por altura estimada, tú la corriges cuando mides, y si te equivocas el scrollTop salta porque nadie más lo va a arreglar. Con content-visibility: auto y contain-intrinsic-size: auto, la mentira es del navegador, se corrige en cuanto el elemento pasa por pantalla una vez, y el anclaje de scroll amortigua la corrección sin que escribas una línea. Es literalmente el mismo problema con dos repartos distintos de responsabilidad, y por eso la comparación correcta entre ambos enfoques nunca es “cuál renderiza más rápido” —los dos evitan el mismo trabajo— sino “cuál tiene mejor comportamiento cuando la estimación falla”, que es siempre. Interiorizar esto cambia el orden en que se evalúan las soluciones: primero preguntas qué pasa cuando el modelo se equivoca, y solo después cuánto ahorra cuando acierta. Un sistema que ahorra el noventa por ciento del trabajo y hace saltar el scroll una vez cada veinte scrolls es peor producto que uno que ahorra el setenta y no salta nunca, porque el usuario no percibe el trabajo ahorrado y percibe el salto con toda claridad.

La búsqueda dentro de la página, las anclas y el índice

Esta es la parte que hay que verificar explícitamente cada vez, porque es donde las soluciones caseras fallan y donde content-visibility gana.

La búsqueda dentro de la página funciona. El navegador busca en el DOM, no en los píxeles, y el DOM está completo: los nodos existen aunque no se hayan maquetado. Cuando hay una coincidencia dentro de una sección saltada, el elemento pasa a ser relevante para el usuario, se renderiza y el navegador desplaza hasta él. El contador de coincidencias —el “3 de 17” de la barra de búsqueda— cuenta todas, incluidas las de secciones no renderizadas.

Las marcas de la barra de scroll se colocan sobre estimaciones. Aquí sí hay un efecto observable: la posición vertical de cada marca depende de la altura acumulada hasta esa coincidencia, y esas alturas son estimadas mientras no se hayan renderizado. Las marcas se reposicionan a medida que las secciones se van midiendo de verdad. Con estimaciones razonables el desplazamiento es de unos píxeles y nadie lo nota; con estimaciones malas, las marcas bailan.

Las anclas y scrollIntoView funcionan, y por el mismo mecanismo: navegar a un fragmento hace relevante al elemento destino. Lo que puede pasar es que el desplazamiento aterrice unos píxeles desviado si las secciones anteriores estaban mal estimadas, y se corrija enseguida.

El árbol de accesibilidad está completo con content-visibility: auto. Un lector de pantalla recorre el documento entero, y la navegación por encabezados llega a secciones que nunca se han pintado. Con content-visibility: hidden no: ahí el contenido está fuera del árbol de accesibilidad, exactamente igual que con display: none, y esa es una decisión de producto, no de rendimiento.

Para los rastreadores de buscadores, el texto está en el DOM y es accesible. Un rastreador que renderiza con un motor moderno ve el contenido de las secciones con auto igual que si fueran visibles; el contenido con hidden recibe el tratamiento que cualquier buscador da al contenido oculto por CSS, que no es el mismo.

La lista de comprobación antes de dar por buena una página con content-visibility, en orden:

  1. Busca con el navegador una palabra que solo aparezca en la última sección. Debe encontrarla y desplazarse.
  2. Navega a un ancla profunda desde otra página. Debe aterrizar bien.
  3. Tabula desde el principio hasta el final. El foco no debe perderse ni saltar.
  4. Recorre la página con un lector de pantalla por encabezados.
  5. Haz scroll hasta el final y vuelve arriba. La barra no debe dar saltos bruscos.
  6. Imprime la página. Debe salir completa.

Si las seis pasan, la optimización es gratis desde el punto de vista del usuario. Si alguna falla, el sitio del fallo casi siempre es la estimación, no la propiedad. El criterio para decidir dónde merece la pena aplicar todo esto está en cuándo aislar ayuda de verdad.

⚔️ Calibra tus estimaciones
  1. Ejecuta estadisticasDeAltura sobre tus secciones y elige el valor de contain-intrinsic-size con criterio. Justifica por qué la p75 y no la mediana.
  2. Pon a propósito una estimación diez veces menor que la real y describe qué le pasa a la barra de scroll. Repite con una diez veces mayor.
  3. Compara el comportamiento con y sin la palabra clave auto haciendo scroll hasta el final y volviendo arriba.
  4. Verifica las seis comprobaciones de la lista sobre una página real.
  5. Genera la estimación en el servidor a partir del número de caracteres de cada sección y compara la calidad con la constante global.