Por qué el layout es un problema global y el navegador no puede acotarlo solo
Cómo se propaga la invalidación por el árbol, por qué el motor no puede deducir que un subárbol es independiente, y qué significa exactamente prometerle que lo es.
Cambiar una palabra dentro de una celda puede obligar al navegador a rehacer el layout del documento entero. No es un fallo de implementación: es una consecuencia directa del modelo de CSS, donde el tamaño de un padre depende del de sus hijos y el de un hijo depende del de su padre. El motor no puede saber por sí solo dónde termina el efecto de un cambio, y por eso existe una familia entera de propiedades cuya única función es que se lo digas tú.
- Explicar por qué el layout es recursivo en las dos direcciones y qué implica para la invalidación.
- Trazar el camino que recorre una invalidación desde el nodo modificado hasta la raíz.
- Definir qué es una garantía de aislamiento y por qué tiene que ser explícita.
- Medir el coste de un recálculo de estilo y de un layout de documento completo en tu propia página.
El layout es recursivo en las dos direcciones
En el flujo normal, el ancho de una caja lo impone el contenedor: fluye de padre a hijo. La altura, en cambio, la determina el contenido: fluye de hijo a padre. Con eso solo ya tienes un sistema de dependencias bidireccional, y cualquier cambio puede propagarse por ambos caminos.
Añade los mecanismos modernos y la cosa se complica más. En un contenedor flexible, el tamaño base de cada elemento influye en cuánto espacio sobra, y el espacio sobrante se reparte entre todos: cada hijo afecta a todos sus hermanos. En una rejilla con pistas auto o min-content, el ancho de una columna lo fija el elemento más ancho de esa columna, así que un texto que crece en la fila nueve puede ensanchar la columna y mover la fila uno. En una tabla con table-layout: auto el algoritmo necesita ver todas las celdas antes de decidir un solo ancho. Y con position: sticky la posición de un elemento depende del scroll de un ancestro, que depende de la altura total del contenido, que depende de todos los descendientes.
Esta red de dependencias es lo que hace que el layout sea el trabajo caro del pipeline. No es que calcular una caja sea difícil: es que no se puede calcular una caja aisladamente sin haber calculado a la vez a sus vecinas, a sus hijas y, muchas veces, a su ancestro.
Ponle número en tu propia página. Este fragmento invalida el documento entero y mide lo que cuesta resolverlo:
function medirDocumentoCompleto() {
// 1. Dejar el arbol limpio para no medir suciedad ajena.
document.body.offsetHeight;
// 2. Invalidar todo: cambiar el tamano de fuente de la raiz obliga a
// recalcular estilo y layout de cada caja del documento.
const anterior = document.documentElement.style.fontSize;
document.documentElement.style.fontSize = '16.001px';
// 3. Forzar la resolucion y cronometrarla.
const t0 = performance.now();
document.body.offsetHeight;
const t1 = performance.now();
document.documentElement.style.fontSize = anterior;
document.body.offsetHeight;
return {
ms: +(t1 - t0).toFixed(2),
nodos: document.querySelectorAll('*').length,
};
}
console.log(medirDocumentoCompleto());
Ejecútalo en una página de documentación sencilla y en el panel de administración de tu aplicación. La diferencia entre ambas no es lineal en el número de nodos: crece con la profundidad del árbol y con el tipo de contenedores que hay por el camino.
Cómo se propaga la invalidación
Cuando tocas un elemento, el motor no marca ese elemento y ya está. Marca lo que puede haber cambiado, y eso depende de qué tocaste.
Hacia abajo. Un cambio en una propiedad heredada —font-size, color, line-height, direction— invalida el estilo de todo el subárbol, porque cada descendiente puede estar heredando ese valor. Un cambio en el ancho del contenedor invalida el layout de todo el subárbol, porque los hijos se dimensionan contra él.
Hacia arriba. Un cambio en el tamaño de un hijo invalida el layout del padre si el padre se dimensiona por su contenido, y desde ahí sigue subiendo mientras la condición se cumpla. En un documento típico, donde casi todos los contenedores tienen altura automática, esa cadena llega hasta el body.
Hacia los lados. En flexbox y en grid, un cambio en un elemento invalida a sus hermanos porque comparten el reparto del espacio. En el flujo normal, cambiar la altura de un bloque mueve a todos los bloques siguientes.
El resultado práctico es que la invalidación tiende a expandirse hasta cubrir el documento. El motor implementa muchísimas optimizaciones para acotarla —marcas separadas para estilo y layout, comprobaciones de si el tamaño resultante coincide con el anterior, reutilización de resultados intermedios— pero todas son heurísticas que trabajan sobre el resultado ya calculado. Ninguna puede decidir antes de calcular que un subárbol no va a afectar a nada de fuera, porque en el caso general eso solo se sabe calculando.
Hay una excepción histórica que conviene conocer porque explica el diseño de todo lo que viene después: el motor sí puede acotar cuando algo tiene tamaño fijo. Si un div tiene width: 300px; height: 200px; overflow: hidden, el motor sabe que su tamaño no depende del contenido y que su contenido no se ve fuera. Ese es, esencialmente, el aislamiento perfecto, y lleva décadas funcionando. El problema es que exige renunciar al dimensionado automático, que es justo lo que hace útil al CSS.
Qué es una garantía de aislamiento
La propiedad contain invierte la relación. En vez de que el navegador deduzca el alcance de un cambio, tú prometes que el alcance está limitado, y a cambio el navegador se permite optimizaciones que de otro modo serían incorrectas.
La palabra clave es permite. Una garantía de aislamiento no es una instrucción de “hazlo más rápido”; es una restricción semántica que cambia el resultado observable. Si prometes que el tamaño del elemento no depende de su contenido y luego el contenido crece, el elemento no crece: se desborda. No es un bug, es lo que pediste.
Esto tiene tres consecuencias que conviene interiorizar antes de escribir una sola declaración de contain.
El coste de una promesa falsa no es lentitud, es un render incorrecto. A diferencia de casi todas las optimizaciones de rendimiento, aquí equivocarte no te devuelve la versión lenta: te devuelve una versión distinta y mal. Una sección que colapsa a cero de alto, una sombra recortada, un modal position: fixed que aparece dentro de una tarjeta en vez de sobre la pantalla.
El beneficio es proporcional al tamaño de lo que aíslas. Contener un span no ahorra nada; el trabajo de mantener la contabilidad del aislamiento es del mismo orden que el trabajo que evita. Contener una sección de mil nodos que se actualiza sola cambia la escala del problema.
El beneficio solo se cobra si algo invalida. Un documento estático, que se renderiza una vez y no vuelve a moverse, no gana nada con contain, porque el primer layout hay que hacerlo entero de todas formas. Donde se cobra es en las actualizaciones: un chat que añade mensajes, un panel que refresca datos, una lista que se reordena.
Merece la pena entender de dónde viene esto, porque explica por qué la solución tiene la forma que tiene y no otra. En un sistema de layout donde todo tamaño fuera explícito —como en la mayoría de los sistemas de interfaz nativos clásicos, o como en un motor de juego— el aislamiento sería trivial: cada caja conoce su rectángulo, y renderizar un subárbol jamás afecta a lo de fuera. CSS eligió lo contrario, y con muy buenas razones: la web tenía que funcionar con contenido de longitud desconocida, en pantallas de tamaño desconocido, con fuentes de métricas desconocidas y en idiomas de dirección desconocida. El dimensionado por contenido es lo que permite que una misma hoja de estilos sirva para un artículo de trescientas palabras y para uno de nueve mil. Es, probablemente, la mejor decisión de diseño de la historia del CSS. Y es también, sin margen de duda, el origen de su mayor coste de rendimiento, porque convierte el layout en un problema con dependencias globales que ninguna optimización local puede romper. Aquí está la lección que va más allá del CSS: la flexibilidad de un sistema declarativo y su capacidad de acotar el trabajo son magnitudes opuestas. Un sistema que promete adaptarse a cualquier contenido está prometiendo, sin decirlo, que no puede saber de antemano cuánto trabajo va a costar renderizarlo. Lo verás igual en un motor de consultas SQL, donde la libertad del optimizador es exactamente lo que hace impredecible el plan, y en cualquier sistema de reglas. Cuando el coste de esa impredecibilidad se vuelve inaceptable, la salida nunca es hacer el motor más listo: es dar al programador una forma de renunciar voluntariamente a un trozo de flexibilidad a cambio de una garantía. contain, content-visibility, los índices de una base de datos y las anotaciones de tipo son la misma idea aplicada a cuatro dominios distintos: información que el humano tiene y la máquina no puede deducir.
Lo que viene y en qué orden
El resto del nivel es el desarrollo de esa idea en herramientas concretas. Los cuatro tipos de contain y qué garantiza exactamente cada uno están en contain y sus cuatro tipos. La propiedad que aplica containment de forma dinámica según lo que el usuario está viendo, en content-visibility. El problema de decirle al navegador cuánto ocupa lo que no ha renderizado, con sus efectos sobre la barra de scroll y sobre la búsqueda dentro de la página, en contain-intrinsic-size. Y el criterio para decidir dónde aplicarlo y dónde no, con medidas, en cuándo aislar ayuda de verdad.
- Ejecuta
medirDocumentoCompleto()en tres páginas distintas y anota milisegundos y número de nodos. Calcula el coste por nodo en cada una y explica la diferencia. - Modifica la función para invalidar solo un subárbol (cambia el
font-sizede un contenedor intermedio) y compara. - Envuelve una parte de la página en un contenedor con
widthyheightfijos yoverflow: hidden, y repite la medición del subárbol. Observa el efecto. - Localiza en tu aplicación un contenedor cuyo tamaño dependa del contenido y que se actualice con frecuencia. Es el primer candidato de todo el nivel.
- Explica por qué un cambio de
colores más barato que uno defont-sizeaunque ambos sean propiedades heredadas.