Guía detallada

Cls Optimization Guide

Ver contenido

Guía Completa de Optimización de CLS: Logrando Estabilidad Visual

Introducción

Cumulative Layout Shift (CLS) mide la estabilidad visual de tu página. Lograr una buena puntuación de CLS (menos de 0.1) requiere entender qué causa los cambios de diseño inesperados e implementar técnicas de optimización específicas.

Esta guía cubre estrategias avanzadas basadas en las recomendaciones oficiales de Google y datos de rendimiento del mundo real para ayudarte a lograr y mantener excelente estabilidad visual.

Entendiendo Qué Causa los Cambios de Diseño

Antes de optimizar, necesitas identificar qué está causando los cambios en tus páginas. Según la documentación de web.dev, las causas más comunes son:

Causas Más Comunes de CLS (por impacto):
├── 1. Imágenes sin dimensiones (35-40% de problemas)
├── 2. Anuncios, embeds e iframes (25-30% de problemas)
├── 3. Contenido inyectado dinámicamente (15-20% de problemas)
├── 4. Fuentes web (FOIT/FOUT) (10-15% de problemas)
└── 5. Animaciones usando propiedades de layout (5-10% de problemas)

Depuración de Cambios de Diseño

Usando el Panel de Performance de Chrome DevTools

  1. Abre DevTools (F12) → pestaña Performance
  2. Marca “Screenshots” y “Web Vitals”
  3. Graba una carga de página
  4. Busca marcadores rojos etiquetados “Layout Shift”
  5. Haz clic en cada marcador para ver qué elementos se movieron
Panel de Performance de DevTools:
┌─────────────────────────────────────────────────┐
│ Timeline: [===LS===LS==========LS====]          │
│                ↑      ↑            ↑             │
│          cambio 1 cambio 2    cambio 3          │
├─────────────────────────────────────────────────┤
│ Detalles del Layout Shift:                      │
│ • Puntuación: 0.08                              │
│ • Elemento: <img class="hero">                  │
│ • Rect Anterior: (0, 0, 1200, 0)               │
│ • Rect Actual: (0, 0, 1200, 600)               │
└─────────────────────────────────────────────────┘

Usando Layout Instability API

Monitorea cambios programáticamente:

// Registra todos los cambios de diseño con información detallada
new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
            console.log('Layout Shift:', {
                value: entry.value,
                sources: entry.sources?.map(source => ({
                    node: source.node,
                    previousRect: source.previousRect,
                    currentRect: source.currentRect
                }))
            });
        }
    }
}).observe({ type: 'layout-shift', buffered: true });

Usando la Librería Web Vitals

import { onCLS } from 'web-vitals';

onCLS((metric) => {
    console.log('CLS:', metric.value);
    console.log('Entries:', metric.entries);

    // Enviar a analytics
    analytics.track('CLS', {
        value: metric.value,
        entries: metric.entries.length,
        url: window.location.href
    });
});

Optimización de Imágenes para CLS

Siempre Especifica Dimensiones

Según MDN Web Docs sobre imágenes responsivas, el manejo apropiado de dimensiones es crítico:

<!-- Método 1: Atributos explícitos de width y height -->
<img src="hero.jpg"
     width="1200"
     height="600"
     alt="Imagen hero">

<!-- Método 2: aspect-ratio en CSS -->
<style>
.hero-image {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
}
</style>
<img src="hero.jpg" class="hero-image" alt="Hero">

<!-- Método 3: Responsivo con srcset (preservando aspect ratio) -->
<img src="hero-800.jpg"
     srcset="hero-400.jpg 400w,
             hero-800.jpg 800w,
             hero-1200.jpg 1200w"
     sizes="(max-width: 600px) 100vw,
            (max-width: 1200px) 80vw,
            1200px"
     width="1200"
     height="600"
     alt="Imagen hero">

Usando el Elemento Picture

Para dirección de arte con diferentes aspect ratios:

<picture>
    <!-- Móvil: Recorte cuadrado -->
    <source media="(max-width: 600px)"
            srcset="hero-mobile.jpg"
            width="600"
            height="600">

    <!-- Escritorio: Recorte ancho -->
    <source media="(min-width: 601px)"
            srcset="hero-desktop.jpg"
            width="1200"
            height="600">

    <img src="hero-desktop.jpg"
         width="1200"
         height="600"
         alt="Hero">
</picture>

Patrón de Aspect Ratio en Contenedor

Reserva espacio antes de que la imagen cargue:

/* Aspect ratio basado en contenedor */
.image-container {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    background: #f0f0f0; /* Color de placeholder */
}

.image-container img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

Manejo de Anuncios y Embeds

Reserva Espacio para Slots de Anuncios

<style>
/* Slots de anuncios de tamaño fijo */
.ad-slot-leaderboard {
    min-height: 90px;   /* Altura estándar de leaderboard */
    width: 100%;
    max-width: 728px;
    background: #f5f5f5;
}

.ad-slot-rectangle {
    min-height: 250px;  /* Rectángulo mediano */
    width: 300px;
    background: #f5f5f5;
}

/* Usa contain-intrinsic-size para mejor rendimiento */
.ad-slot {
    contain: layout style;
    contain-intrinsic-size: 0 250px;
}
</style>

<div class="ad-slot ad-slot-rectangle">
    <!-- El anuncio carga aquí -->
</div>

Contenedores de Anuncios Sticky

Previene que los anuncios empujen contenido:

/* Anuncio sticky en sidebar que no mueve contenido */
.sticky-ad {
    position: sticky;
    top: 20px;
    min-height: 600px;
}

/* O usa posicionamiento absoluto dentro de espacio reservado */
.ad-wrapper {
    position: relative;
    min-height: 250px;
}

.ad-wrapper .ad-content {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
}

Manejo de Embeds

<!-- Embed de YouTube con aspect ratio -->
<style>
.video-container {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    background: #000;
}

.video-container iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}
</style>

<div class="video-container">
    <iframe src="https://www.youtube.com/embed/VIDEO_ID"
            frameborder="0"
            allowfullscreen
            loading="lazy"></iframe>
</div>

<!-- Embed de Twitter con espacio reservado -->
<style>
.twitter-container {
    min-height: 500px; /* Altura promedio de tweet */
    contain: layout;
}
</style>

<div class="twitter-container">
    <blockquote class="twitter-tweet">...</blockquote>
</div>

Estrategias para Contenido Dinámico

Arriba vs Abajo del Viewport

Inserta contenido dinámico debajo del viewport cuando sea posible:

// ❌ Malo: Insertar arriba, causa cambio
function addBanner() {
    const banner = createBanner();
    document.body.insertBefore(banner, document.body.firstChild);
}

// ✅ Bueno: Reserva espacio primero
function addBanner() {
    const container = document.querySelector('.banner-slot');
    container.style.minHeight = '60px'; // Reserva primero
    container.innerHTML = createBannerHTML();
}

// ✅ Mejor: Solo agregar debajo del viewport o con interacción de usuario
function showNotification() {
    const notification = createNotification();
    // Añadir al final de la página, no mueve contenido existente
    document.body.appendChild(notification);
}

Skeleton Screens

Usa skeleton loaders que coincidan con las dimensiones finales del contenido:

<style>
.skeleton {
    background: linear-gradient(
        90deg,
        #f0f0f0 25%,
        #e0e0e0 50%,
        #f0f0f0 75%
    );
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

.card-skeleton {
    height: 300px; /* Coincide con altura real de la tarjeta */
    border-radius: 8px;
}

.text-skeleton {
    height: 20px;
    margin: 10px 0;
    border-radius: 4px;
}
</style>

<div class="card-skeleton skeleton"></div>

Listas Virtualizadas

Para listas largas, usa scroll virtual:

// Concepto: Solo renderiza items visibles
class VirtualList {
    constructor(container, itemHeight, totalItems) {
        this.container = container;
        this.itemHeight = itemHeight;
        this.totalItems = totalItems;

        // Establece altura total para prevenir cambio
        this.container.style.height = `${itemHeight * totalItems}px`;
        this.container.style.position = 'relative';
    }

    render(scrollTop) {
        const startIndex = Math.floor(scrollTop / this.itemHeight);
        const endIndex = Math.min(
            startIndex + this.visibleCount,
            this.totalItems
        );
        // Solo renderiza items desde startIndex hasta endIndex
    }
}

Optimización de Carga de Fuentes

Precarga Fuentes Críticas

<head>
    <!-- Precarga la fuente más importante -->
    <link rel="preload"
          href="/fonts/primary.woff2"
          as="font"
          type="font/woff2"
          crossorigin>
</head>

Estrategias de Font-Display

/* Opción 1: optional - Mejor para CLS (puede no mostrar fuente personalizada) */
@font-face {
    font-family: 'FuentePersonalizada';
    src: url('/fonts/custom.woff2') format('woff2');
    font-display: optional;
}

/* Opción 2: swap con size-adjust - Buen balance */
@font-face {
    font-family: 'FuentePersonalizada';
    src: url('/fonts/custom.woff2') format('woff2');
    font-display: swap;
    size-adjust: 105%; /* Ajusta para coincidir con fallback */
    ascent-override: 90%;
    descent-override: 20%;
    line-gap-override: 0%;
}

/* Opción 3: Usa stack de fuentes del sistema como fallback con métricas coincidentes */
body {
    font-family: 'FuentePersonalizada', -apple-system, BlinkMacSystemFont,
                 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

Overrides de Métricas de Fuente

Haz coincidir las métricas de la fuente de fallback con la fuente personalizada:

@font-face {
    font-family: 'Roboto';
    src: url('/fonts/roboto.woff2') format('woff2');
    font-display: swap;
    /* Estos valores deben coincidir con las métricas de tu fuente personalizada */
    ascent-override: 92.7%;
    descent-override: 24.4%;
    line-gap-override: 0%;
}

Mejores Prácticas de Animación

Usa Transform y Opacity

/* ❌ Causa cambios de diseño */
.animate-bad {
    animation: slideIn 0.3s;
}
@keyframes slideIn {
    from { margin-left: -100px; }
    to { margin-left: 0; }
}

/* ✅ Sin cambios de diseño */
.animate-good {
    animation: slideIn 0.3s;
}
@keyframes slideIn {
    from { transform: translateX(-100px); }
    to { transform: translateX(0); }
}

Propiedades Aceleradas por Hardware

Solo estas propiedades no causan layout:

  • transform
  • opacity
  • filter
  • will-change (usar con moderación)
.smooth-animation {
    /* Indica al navegador que optimice */
    will-change: transform;
    transition: transform 0.3s ease;
}

/* O usa contain para aislamiento */
.contained-animation {
    contain: layout style paint;
}

Content-Visibility para Rendimiento

Usa content-visibility para saltar el renderizado de contenido fuera de pantalla:

/* Salta renderizado hasta estar cerca del viewport */
.below-fold-section {
    content-visibility: auto;
    contain-intrinsic-size: 0 500px; /* Altura estimada */
}

/* Más agresivo para contenido muy abajo */
.footer {
    content-visibility: auto;
    contain-intrinsic-size: 0 300px;
}

Monitoreo de CLS en Producción

Real User Monitoring (RUM)

import { onCLS } from 'web-vitals';

function sendToAnalytics(metric) {
    const body = JSON.stringify({
        name: metric.name,
        value: metric.value,
        delta: metric.delta,
        id: metric.id,
        page: window.location.pathname,
        entries: metric.entries.length
    });

    // Usa sendBeacon para confiabilidad
    if (navigator.sendBeacon) {
        navigator.sendBeacon('/analytics', body);
    } else {
        fetch('/analytics', {
            body,
            method: 'POST',
            keepalive: true
        });
    }
}

onCLS(sendToAnalytics);

Alertas por Regresión

// Configura alertas por umbral
onCLS((metric) => {
    if (metric.value > 0.1) {
        console.warn('Umbral de CLS excedido:', metric.value);
        // Envía alerta al sistema de monitoreo
        alertMonitoring('cls_regression', {
            value: metric.value,
            threshold: 0.1,
            page: window.location.href
        });
    }
});

Lista de Verificación de Optimización de CLS

Imágenes y Media
├── [ ] Todas las imágenes tienen atributos width y height
├── [ ] Imágenes responsivas usan srcset con sizes
├── [ ] Embeds de video tienen contenedores con aspect-ratio
├── [ ] Imágenes lazy-loaded tienen dimensiones de placeholder

Contenido Dinámico
├── [ ] Slots de anuncios tienen altura mínima reservada
├── [ ] Embeds tienen contenedores con aspect-ratio
├── [ ] Banners dinámicos tienen espacio pre-reservado
├── [ ] Skeleton screens coinciden con dimensiones finales

Fuentes
├── [ ] Fuentes críticas están precargadas
├── [ ] Se usa font-display: optional o swap
├── [ ] Fuentes de fallback tienen métricas coincidentes
├── [ ] La carga de fuentes no causa FOUT

Animaciones
├── [ ] Solo se animan transform y opacity
├── [ ] Sin propiedades que disparan layout en animaciones
├── [ ] will-change usado apropiadamente
├── [ ] contain usado para aislamiento de animación

Monitoreo
├── [ ] CLS rastreado con librería web-vitals
├── [ ] Datos de campo recolectados via RUM
├── [ ] Alertas configuradas para regresiones de CLS
├── [ ] Auditorías regulares con DevTools

Artículos Relacionados

Referencias

Este artículo fue mejorado usando fuentes autoritativas:

[1] web.dev: Optimize Cumulative Layout Shift https://web.dev/articles/optimize-cls Guía completa de Google para optimización de CLS. Cubre las principales causas de cambios de diseño y técnicas específicas para cada una: dimensiones de imagen, manejo de anuncios, inserción de contenido dinámico, carga de fuentes, y mejores prácticas de animación.

[2] MDN Web Docs: Responsive Images https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Responsive_images Documentación técnica sobre implementación de imágenes responsivas que mantienen aspect ratios apropiados. Cubre srcset, sizes, elemento picture, y manejo apropiado de dimensiones para prevenir cambios de diseño.

[3] web.dev: Debugging Layout Shifts https://web.dev/articles/debug-layout-shifts Guía completa para identificar fuentes de cambio de diseño usando Chrome DevTools, Layout Instability API, y monitoreo de rendimiento. Esencial para depuración sistemática de CLS.

[4] HTTP Archive Web Almanac: Performance https://almanac.httparchive.org/en/2024/performance Datos de la industria mostrando que las imágenes sin dimensiones representan el 35-40% de los problemas de CLS, con anuncios/embeds causando el 25-30%. Estadísticas del mundo real informan las prioridades de optimización.


Nota: Este artículo es parte de nuestra serie de análisis SEO. Explora todos los artículos en el Hub de SEO de Rendimiento.


Fuentes: web.dev (Optimización de CLS), MDN Web Docs, HTTP Archive Web Almanac

Artículos relacionados

Versión relacionada

Introducción

Cls Explained

Cumulative Layout Shift (CLS) es uno de los Core Web Vitals de Google—un conjunto de métricas que miden la experiencia real del usuario en tu sitio web

Hub de categoría

Hub

Performance Seo Hub

El rendimiento es un factor de ranking crítico e impacta directamente la experiencia del usuario

En la misma categoría

Guía detallada

Lcp Optimization Guide

Largest Contentful Paint (LCP) mide cuándo el elemento de contenido más grande se vuelve visible para los usuarios

Guía detallada

Viewport Optimization Guide

El viewport meta tag es más que un simple elemento HTML—es la base del diseño web responsivo y la optimización móvil