Cuándo aislar ayuda de verdad y cuándo es ruido
Los tres perfiles de página donde el containment paga, cómo medir el efecto con el número de nodos en vez de con milisegundos, y los sitios donde no hay que ponerlo nunca.
El containment no hace que el renderizado sea más rápido: hace que su coste sea proporcional a lo que cambió en vez de al tamaño del documento. Esa distinción decide dónde aplicarlo. En una página corta y estática el beneficio es exactamente cero, y en un panel de administración que refresca datos cada segundo dentro de un DOM de ocho mil nodos puede ser la diferencia entre sesenta fotogramas por segundo y doce. Esta lección da el criterio y la forma de medirlo sin engañarse.
- Reconocer los tres perfiles de página en los que el containment produce una ganancia real.
- Medir el efecto contando nodos afectados y no solo milisegundos.
- Identificar los lugares donde aplicar
content-visibilityempeora las métricas. - Elaborar una lista de contenedores candidatos con un criterio defendible.
Los tres perfiles donde paga
Perfil uno: la página larga. Un artículo con cincuenta secciones, una documentación de una sola página, un hilo de foro con cuatrocientos mensajes, un histórico de cambios. El usuario ve el cinco por ciento del contenido y el navegador maqueta y prepara el pintado del cien por cien. Aquí content-visibility: auto con una estimación decente es la mejor relación entre esfuerzo y resultado de todo el track: una regla de CSS y una lista de comprobación.
Perfil dos: el widget que se actualiza dentro de un documento grande. Un contador en vivo, una tabla que refresca cada segundo, un chat que recibe mensajes, un gráfico que anima. Cada actualización invalida el layout, la invalidación sube hasta la raíz, y el navegador recalcula un documento entero para mover un número. Aquí la herramienta es contain: content o contain: strict sobre el widget, y el efecto se ve en el número de nodos que el motor procesa por actualización, que puede caer de miles a decenas.
Perfil tres: la lista o la tabla con muchas filas. Miles de nodos con la misma estructura. El containment por fila corta la propagación entre hermanas, y content-visibility: auto por fila salta el trabajo de las que no se ven. Es el escalón previo a la virtualización de verdad, no el sustituto; dónde está la frontera lo trata el coste de diez mil nodos.
Fuera de estos tres, el reparto es sencillo. Una página corta no gana nada: el layout completo cuesta poco y hay que hacerlo entero de todas formas. Un documento estático que se renderiza una vez y no vuelve a cambiar tampoco: el ahorro del containment está en las actualizaciones, y no hay ninguna.
Cómo medir el efecto de verdad
Medir el containment con un cronómetro es engañoso, porque la variabilidad de una sola medida es del mismo orden que el efecto que buscas. Lo que sí es estable y comparable es el trabajo que el motor declara haber hecho.
En el panel de rendimiento, graba la interacción que te preocupa y selecciona los eventos:
- En un evento de recálculo de estilo, el resumen indica el número de elementos afectados. Ese número es determinista para una interacción dada, así que se puede comparar antes y después sin ruido.
- En un evento de layout, el resumen indica el tamaño del árbol procesado y si el ámbito fue parcial o de documento completo. El salto de “documento completo” a “parcial” es la señal de que la contención está funcionando.
- El número de eventos de cada tipo por interacción. Debería quedarse igual; si crece, algo se ha roto.
Si prefieres una medida que puedas automatizar, cuenta el trabajo agregado con Long Animation Frames, que separa el tiempo de estilo y layout del resto:
// Tiempo de estilo y layout por fotograma largo, agregado.
let totalEstiloLayout = 0;
let fotogramas = 0;
new PerformanceObserver((lista) => {
for (const f of lista.getEntries()) {
fotogramas++;
// styleAndLayoutStart marca el inicio de la fase; el resto del
// fotograma hasta startTime + duration es estilo, layout y pintado.
if (f.styleAndLayoutStart) {
totalEstiloLayout += f.startTime + f.duration - f.styleAndLayoutStart;
}
}
}).observe({ type: 'long-animation-frame', buffered: true });
// Tras ejecutar el escenario:
setTimeout(() => {
console.log('fotogramas largos', fotogramas,
'ms de estilo+layout', Math.round(totalEstiloLayout));
}, 10000);
Y para una comparación limpia entre dos versiones de la misma página, el procedimiento honesto es este: mismo dispositivo, mismo throttling, recarga completa entre medidas, cinco repeticiones, mediana. Con menos de eso, cualquier diferencia por debajo del veinte por ciento es ruido y lo sabes.
Hay un contraste que conviene hacer siempre y que casi nadie hace: mide también el caso peor. El containment cambia el reparto del trabajo, y a veces lo concentra. Con content-visibility: auto, el scroll rápido a través de una página larga obliga al navegador a maquetar secciones a toda velocidad; si tus secciones son enormes, puedes cambiar un renderizado inicial lento por unos tirones durante el scroll. Prueba el scroll con la rueda a fondo, no solo la carga.
Aquí está la formulación que hace que todo el nivel encaje. Sin containment, el coste de una actualización es proporcional al tamaño del documento, porque la invalidación se propaga hasta donde el motor no pueda demostrar que se detiene, y en el caso general eso es la raíz. Con containment, el coste es proporcional al tamaño de lo que cambió. Escrito así se ve que no estás optimizando una constante: estás cambiando la variable de la que depende el coste. Y de ahí salen, directamente y sin más razonamiento, las tres reglas que la gente aprende a base de golpes. Primera: el beneficio crece con el tamaño del documento, así que la misma línea de CSS que no hace nada en tu página de pruebas transforma la de producción; nunca evalúes containment en un caso pequeño. Segunda: el beneficio crece con la frecuencia de actualización, así que un componente que cambia sesenta veces por segundo lo justifica y uno que cambia una vez por sesión no; el criterio no es lo grande que sea el componente sino cuántas veces invalida. Tercera, y es la contraintuitiva: si tu documento es pequeño, el containment puede salir negativo, porque el motor paga la contabilidad del aislamiento —contextos de apilamiento, contenedores de bloque, seguimiento de relevancia— para evitar un trabajo que costaba menos que esa contabilidad. Esta forma de pensar no es exclusiva del CSS. Es exactamente el razonamiento que hay detrás de un índice en una base de datos, que no acelera nada por sí mismo sino que cambia una búsqueda lineal por una logarítmica, y que en una tabla de doscientas filas es más lento que el escaneo. Cuando alguien te pregunte si debe poner contain en algo, la respuesta correcta nunca es sí o no: es “¿cuántos nodos hay fuera y cuántas veces por segundo invalida?”.
Dónde no aplicarlo nunca
En el primer viewport. El elemento que va a ser tu LCP no puede estar dentro de un subárbol con content-visibility: auto, ni debe estar cerca de la frontera. Aunque el navegador lo renderice por estar en pantalla, has añadido una comprobación de relevancia y una capa de contención en el camino crítico del pintado que más importa, y el riesgo de retrasarlo es real y el beneficio es nulo, porque ese contenido hay que renderizarlo sí o sí. La regla es tajante: content-visibility: auto empieza por debajo del pliegue.
En elementos pequeños y en selectores genéricos. Ya está argumentado en la lección anterior: el ahorro es proporcional al subárbol y la contabilidad no. Contenedores de decenas o cientos de nodos, enumerados a mano.
En contenido que se va a necesitar entero de todas formas. Si la página se imprime, se exporta a PDF o se procesa con una herramienta que lee el renderizado, el navegador tendrá que maquetarlo todo igualmente. Chrome renderiza el contenido con content-visibility: auto al imprimir, así que no se rompe, pero el ahorro desaparece en ese flujo.
En elementos que necesitan que algo se salga. Contenedores de menús desplegables, de tooltips, de badges con desbordamiento, o que alberguen un position: sticky que deba pegarse al viewport. El recorte de la contención de pintado no se puede desactivar selectivamente.
Dentro de un contenedor ya virtualizado. Si una librería de listas ya se ocupa de no montar los nodos invisibles, añadir content-visibility: auto a cada fila montada es contabilidad sobre contabilidad: esas filas son justo las que sí están en pantalla.
La lista de decisión
Para cada candidato, cinco preguntas. Si las cinco respuestas van en la misma dirección, la decisión está tomada.
- ¿Cuántos nodos hay dentro? Menos de treinta, descártalo.
- ¿Cuántas veces por minuto invalida su interior? Cero y contenido estático, descártalo salvo que esté fuera de pantalla en una página larga.
- ¿Está por debajo del primer viewport? Si no lo está, nada de
content-visibility; como muchocontain. - ¿Se sale algo de sus bordes? Si sí,
contain: layout stylesinpaint, o nada. - ¿Puedo estimar su altura con menos de un cincuenta por ciento de error? Si no,
contain: contentsin contención de tamaño.
Y una regla de higiene que ahorra discusiones seis meses después: cada declaración de contain o de content-visibility en el código lleva un comentario de una línea que diga qué se está prometiendo y por qué es cierto. No “para que vaya más rápido”, sino “esta tarjeta no contiene nada posicionado en fixed ni con desbordamiento visible”.
/* Contencion segura: la tarjeta no tiene descendientes fixed, su unico
desbordamiento es la sombra, que ya recortamos con border-radius, y
se actualiza cuando llegan datos por websocket. */
.tarjeta-cotizacion { contain: content; }
/* Solo por debajo del pliegue. La estimacion sale de la p75 medida
sobre 400 comentarios reales: 210px. */
.comentario:nth-child(n + 8) {
content-visibility: auto;
contain-intrinsic-block-size: auto 210px;
}
Ese selector con nth-child es un truco que merece la pena conocer: aplica el salto de renderizado solo a partir del octavo elemento, dejando los primeros —los que caen en el primer viewport— completamente normales. Consigue la protección del LCP sin duplicar clases ni tocar el marcado.
- Recorre tu aplicación y anota todos los contenedores con más de cien nodos. Ordénalos por frecuencia de actualización.
- Aplica
contain: contental primero de la lista y compara el número de elementos afectados por recálculo de estilo antes y después. - Aplica
content-visibility: autoa las secciones por debajo del pliegue de tu página más larga y comprueba que el LCP no empeora. - Mide el caso peor: scroll rápido de arriba abajo. Anota si aparecen fotogramas largos que antes no estaban.
- Escribe el comentario justificativo de cada declaración que hayas añadido. Si no sabes qué escribir, quita la declaración.