En optimización web solemos centrarnos en la carga de recursos: optimizar imágenes, minificar JavaScript, reducir el CSS crítico, etc. Todas ellas muy importantes, sin embargo, hay una fase del proceso que a veces pasamos por alto y que tiene un impacto directo en la fluidez de la experiencia de navegación: el renderizado.
El navegador trabaja para transformar el código HTML y CSS en los píxeles que vemos en pantalla. Este proceso incluye el cálculo de estilos, la maquetación (Layout), el pintado (Paint) y la composición (Composite). Cuando tenemos páginas con mucho contenido, como listas infinitas o artículos muy extensos, el navegador paga el “peaje” de renderizar todo ese contenido, incluso si está fuera de la pantalla (off-screen) y quien navega aún no ha llegado a él.
En este artículo vamos a profundizar en content-visibility, una propiedad CSS que puede mejorar significativamente el rendimiento de renderizado. Y os compartiré un WebPerf Snippets para que podáis auditar su uso y detectar dónde aplicarlo.
¿Qué es content-visibility y por qué nos debería importar?
La propiedad CSS content-visibility permite al navegador saber que un elemento (y su contenido descendiente) puede saltarse el trabajo de renderizado hasta que sea necesario, es decir, hasta que se acerque al área visible (viewport).
Cuando aplicamos content-visibility: auto a un elemento, el navegador puede posponer el cálculo de layout y el pintado de ese elemento si no está visible. Es la misma técnica que usan los videojuegos: no renderizar los polígonos que están fuera del encuadre de la cámara. Conceptualmente es similar al lazy-loading de imágenes, pero aplicado al renderizado del propio DOM.
Esto tiene beneficios inmediatos:
- Mejora el tiempo de carga inicial (Initial Load Time): Al tener que procesar menos elementos inicialmente, el hilo principal se libera antes.
- Mejora la interacción: Menos trabajo en el hilo principal significa una respuesta más rápida a las interacciones de quien navega.
- Desplazamiento más fluido: Al reducir la complejidad del DOM activo, el scroll se percibe más suave.
Implementación básica
Para implementar esto, generalmente necesitamos dos propiedades: content-visibility y contain-intrinsic-size. Esta última es crucial porque, si el navegador no renderiza el elemento, este técnicamente tendría una altura de 0px, lo que causaría problemas con la barra de desplazamiento (saltos o un tamaño incorrecto), provocando una mala experiencia, que se vería reflejada en la métrica CLS. Con contain-intrinsic-size le damos al navegador una estimación del tamaño que ocupará el elemento.
.card {
content-visibility: auto;
contain-intrinsic-size: 1px 1000px; /* Ancho estimado y alto estimado */
}
Un caso real: Optimizando una lista de componentes en React
Para entender mejor el impacto de esta optimización, veamos un ejemplo real del React Workshop Fwdays (Marzo 2024) (podéis ver la PR completa aquí).
En este proyecto, teníamos una lista larga de componentes llamados NoteButton. Al renderizar la lista completa, el navegador tenía que calcular el layout y pintar cientos de elementos, incluso aquellos que estaban muy por debajo del área visible de la pantalla. Esto generaba un cuello de botella significativo en el hilo principal durante la carga y el scroll.
El Problema
Sin content-visibility, las herramientas de desarrollo mostraban un tiempo considerable dedicado al “Layout” y “Rendering” de toda la lista. El navegador estaba calculando la posición y estilo de cada botón de nota antes de que quien navega pudiera siquiera verlos.
Además, al inspeccionar las capas (Layers) de la página, podíamos ver una cantidad masiva de contenido pintado listo para ser mostrado, consumiendo memoria innecesariamente.
La Solución
La solución fue sencilla pero efectiva. Aplicamos content-visibility: auto y contain-intrinsic-size al contenedor de cada nota.
.note-button {
content-visibility: auto;
contain-intrinsic-size: 1px 200px; /* Estimación del tamaño de la nota (componente React) */
}
El Resultado
El cambio fue notable. El navegador pasó a calcular solo el layout de los elementos visibles en el viewport. Los elementos fuera de pantalla quedaron en un estado “dormido”, ocupando espacio (gracias a contain-intrinsic-size) pero sin costar tiempo de procesamiento.
En la visualización de capas, ahora vemos que solo se pintan los elementos necesarios. A medida que hacemos scroll, el navegador “despierta” los nuevos elementos justo a tiempo (como podemos ver, hay margen antes de entrar en el viewport, esto lo gestiona el navegador de forma automática y transparente).
El siguiente vídeo ilustra perfectamente este comportamiento dinámico. Observad cómo las capas se crean y destruyen bajo demanda mientras se hace scroll, manteniendo el uso de memoria y CPU al mínimo.
Este caso demuestra que content-visibility no es solo teoría; es una herramienta práctica que puede resolver problemas reales de rendimiento en aplicaciones con listas largas, contenido complejo o contenido extenso, como una landing.
Auditando nuestra web: El WebPerf Snippet
Ahora que entendemos la teoría y hemos visto un caso real, surge la necesidad práctica: ¿Cómo sabemos qué elementos de nuestra web están utilizando esta propiedad? O, si estamos auditando una web de terceros o un proyecto heredado, ¿cómo podemos identificar rápidamente dónde se está aplicando esta optimización?
Para ello, usaremos un WebPerf Snippet (script de JavaScript) que podemos ejecutar directamente en la consola de las DevTools. Este script recorre el DOM y busca aquellos elementos que tienen la propiedad content-visibility computada con el valor auto.
Aquí tenéis el código del snippet:
function findContentVisibilityElements() {
// Get all DOM elements
const allElements = document.querySelectorAll("*");
const results = [];
// Iterate through each element to check its computed style
allElements.forEach(el => {
const style = window.getComputedStyle(el);
if (style.contentVisibility === "auto") {
// If we find the property, save useful information
results.push({
element: el,
tagName: el.tagName.toLowerCase(),
class: el.className,
id: el.id,
intrinsicSize: style.containIntrinsicSize,
});
}
});
// Display the results
if (results.length > 0) {
console.group("🔍 Elements with content-visibility: auto detected");
console.table(
results.map(item => ({
Tag: item.tagName,
Class: item.class,
ID: item.id,
"Intrinsic Size": item.intrinsicSize,
}))
);
console.log("Total elements found:", results.length);
console.log(
"Element references (you can interact with them):",
results.map(r => r.element)
);
console.groupEnd();
} else {
console.log("❌ No elements with content-visibility: auto found.");
}
}
// Run the function
findContentVisibilityElements();
Cómo funciona el script
El funcionamiento es sencillo pero eficaz:
- Selección Universal: Utilizamos
document.querySelectorAll('*')para obtener una lista de todos los nodos del documento. - Análisis de Estilos: Para cada elemento, usamos
window.getComputedStyle(el). Esto es importante porque no solo buscamos estilos en línea o en hojas de estilo, sino el valor final que el navegador está aplicando, lo cual refleja el estado real del renderizado. - Filtrado: Comprobamos si la propiedad
contentVisibilityes igual a'auto'. - Reporte: Recopilamos datos relevantes como la etiqueta, clases, ID y, muy importante, el valor de
contain-intrinsic-size(para verificar que se está usando correctamente junto concontent-visibility). Finalmente, mostramos una tabla ordenada en la consola.
Interpretando los resultados
Al ejecutar este script en la consola de vuestro navegador, podréis ver de un vistazo qué secciones de la página se están beneficiando de esta optimización.
Si veis elementos con content-visibility: auto pero donde la columna Intrinsic Size aparece vacía o con valores por defecto que no encajan, podríamos estar ante un problema de implementación que cause saltos en el scroll (layout shifts).
Este tipo de auditorías son fundamentales. A menudo implementamos mejoras de rendimiento que, con el tiempo y los cambios en el código, pueden dejar de funcionar o aplicarse incorrectamente. Tener herramientas para visualizar el estado real de la aplicación es clave para mantener una buena salud del proyecto.
Encontrando candidatos para aplicar content-visibility
¿Y si nuestra web aún no utiliza esta propiedad? ¿Por dónde empezamos? Identificar manualmente qué bloques de contenido son lo suficientemente complejos y están fuera de la pantalla inicial para justificar esta optimización puede ser tedioso.
Para facilitar esta tarea, disponemos de una variante del snippet anterior. Este nuevo script escanea el DOM en busca de candidatos potenciales.
¿Qué criterios utiliza?
- Está fuera del viewport: Elementos que no se ven inicialmente.
- Complejidad: Elementos que tienen un número significativo de descendientes (por defecto > 10), lo que implica un coste de renderizado que merece la pena ahorrar.
- Tipo de elemento: Se centra en elementos de bloque comunes como
div,section,article,li, etc.
Aquí tenéis el snippet para encontrar oportunidades de mejora:
function findPotentialCandidates(minChildren = 10) {
const candidates = [];
// Select common container elements
const elements = document.querySelectorAll(
"div, section, article, li, ul, ol, main, aside"
);
const viewportHeight = window.innerHeight;
elements.forEach(el => {
// Get element position
const rect = el.getBoundingClientRect();
// Check if it's completely outside the viewport (below the fold)
const isOffScreen = rect.top > viewportHeight;
// Check if it has enough complexity (number of descendants)
// We use getElementsByTagName('*').length to count all child nodes
const complexity = el.getElementsByTagName("*").length;
if (isOffScreen && complexity >= minChildren) {
// Filter to avoid suggesting parent containers that only contain other already listed candidates
// This is a simple heuristic, in a real case we would want to refine it
candidates.push({
element: el,
tagName: el.tagName.toLowerCase(),
class: el.className,
complexity: complexity,
distanceFromViewport: Math.round(rect.top - viewportHeight) + "px",
});
}
});
// Sort by complexity (highest potential savings first)
candidates.sort((a, b) => b.complexity - a.complexity);
if (candidates.length > 0) {
console.group("🚀 Potential candidates for content-visibility: auto");
console.log(
`Criteria: Outside viewport and more than ${minChildren} descendants.`
);
console.table(
candidates.map(c => ({
Tag: c.tagName,
Class: c.class.substring(0, 30) + (c.class.length > 30 ? "..." : ""), // Truncate long classes
"Complexity (nodes)": c.complexity,
"Distance from Viewport": c.distanceFromViewport,
}))
);
console.log(
"Element references:",
candidates.map(c => c.element)
);
console.groupEnd();
} else {
console.log("👍 No obvious candidates found with the current criteria.");
}
}
// Run the search
findPotentialCandidates();
Este script es una herramienta de análisis. No significa que debamos aplicar content-visibility: auto a todo lo que aparezca en la lista, pero nos da un excelente punto de partida para identificar las secciones “pesadas” de nuestra web que están ralentizando la carga inicial sin aportar valor inmediato a quien navega.
Conclusión
La propiedad content-visibility es una de esas herramientas modernas de CSS que nos permiten obtener grandes ganancias de rendimiento con muy poco esfuerzo. Nos ayuda a gestionar la carga de trabajo del navegador de una manera inteligente, priorizando lo que ven los usuarios y usuarias.
Con los snippets que hemos visto, ahora tenemos herramientas tanto para verificar la implementación existente como para descubrir nuevas oportunidades de optimización. Os animo a probarlos en vuestros proyectos y a experimentar con esta propiedad en aquellas secciones de contenido extenso que a menudo penalizan el rendimiento.