Guía detallada

Inp Optimization Guide

Ver contenido

Guía Completa de Optimización de INP: Logrando Respuesta Rápida de Página

Introducción

Interaction to Next Paint (INP) mide qué tan rápido responde tu página a las interacciones del usuario. Lograr una buena puntuación de INP (menos de 200ms) requiere entender las tres fases de latencia de interacción y aplicar técnicas de optimización específicas.

Esta guía cubre estrategias avanzadas basadas en las recomendaciones oficiales de Google y APIs modernas del navegador para ayudarte a lograr respuestas rápidas y consistentes.

Entendiendo las Fases de INP

Cada interacción pasa por tres fases:

Desglose de Interacción (presupuesto de 200ms):

┌─────────────────────────────────────────────────────────┐
│ 1. RETRASO DE ENTRADA  │ Tiempo hasta que inician los  │
│    Objetivo: <50ms     │ manejadores. Causado por:     │
│                        │ bloqueo del hilo principal    │
├─────────────────────────────────────────────────────────┤
│ 2. TIEMPO DE           │ Tiempo para ejecutar los      │
│ PROCESAMIENTO          │ manejadores. Causado por:     │
│    Objetivo: <100ms    │ manejadores de eventos pesados│
├─────────────────────────────────────────────────────────┤
│ 3. RETRASO DE          │ Tiempo para renderizar la     │
│ PRESENTACIÓN           │ actualización. Causado por:   │
│    Objetivo: <50ms     │ renderizado complejo          │
└─────────────────────────────────────────────────────────┘

Depuración de Problemas de INP

Usando el Panel de Performance de Chrome DevTools

  1. Abre DevTools → pestaña Performance
  2. Habilita la casilla “Web Vitals”
  3. Graba mientras interactúas con la página
  4. Busca:
    • Long Tasks (> 50ms) resaltadas en rojo
    • Marcadores de interacción mostrando duración de INP
    • Manejadores de eventos tomando tiempo excesivo
Análisis de DevTools:
┌─────────────────────────────────────────────────┐
│ Interacción: clic en botón                      │
├─────────────────────────────────────────────────┤
│ Retraso de Entrada: 45ms (✅ Bueno)             │
│ Procesamiento:      180ms (❌ Muy largo)        │
│ Presentación:       25ms (✅ Bueno)             │
│ INP Total:          250ms (⚠️ Necesita Mejora) │
├─────────────────────────────────────────────────┤
│ Problema: Manejador "handleSubmit" tomó 180ms  │
│ Solución: Dividir u optimizar manejador        │
└─────────────────────────────────────────────────┘

Usando Long Animation Frames API

Monitorea frames largos programáticamente:

// Detecta frames que perjudican INP
const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        // Long Animation Frames > 50ms
        console.log('Frame Largo:', {
            duration: entry.duration,
            blockingDuration: entry.blockingDuration,
            scripts: entry.scripts.map(s => ({
                invoker: s.invoker,
                duration: s.duration
            }))
        });
    }
});

observer.observe({ type: 'long-animation-frame', buffered: true });

Usando Atribución de Web Vitals Library

import { onINP } from 'web-vitals/attribution';

onINP((metric) => {
    console.log('INP:', metric.value);
    console.log('Atribución:', {
        eventTarget: metric.attribution.eventTarget,
        eventType: metric.attribution.eventType,
        loadState: metric.attribution.loadState,
        eventTime: metric.attribution.eventTime,
        presentationDelay: metric.attribution.presentationDelay,
        processingDuration: metric.attribution.processingDuration,
        inputDelay: metric.attribution.inputDelay
    });
});

Reduciendo el Retraso de Entrada

El retraso de entrada ocurre cuando el hilo principal está ocupado cuando el usuario interactúa.

1. Divide Tareas Largas

Según la documentación de web.dev, las tareas de más de 50ms son “tareas largas” que bloquean el hilo principal:

// ❌ Tarea larga bloquea el hilo principal
function processAllData(data) {
    data.forEach(item => {
        heavyProcessing(item); // Ejecuta por 500ms en total
    });
}

// ✅ Divide en chunks que ceden al hilo principal
async function processAllDataYielding(data) {
    const chunkSize = 50;

    for (let i = 0; i < data.length; i += chunkSize) {
        const chunk = data.slice(i, i + chunkSize);
        chunk.forEach(item => heavyProcessing(item));

        // Cede al hilo principal entre chunks
        await yieldToMain();
    }
}

// Función moderna de yield usando scheduler.yield
async function yieldToMain() {
    if ('scheduler' in window && 'yield' in scheduler) {
        return scheduler.yield();
    }
    // Fallback para navegadores antiguos
    return new Promise(resolve => setTimeout(resolve, 0));
}

2. Usa la Scheduler API

La Scheduler API proporciona control granular sobre la prioridad de tareas:

// Programa tareas por prioridad
async function handleUserAction() {
    // Alta prioridad: Retroalimentación visual inmediata
    await scheduler.postTask(() => {
        button.classList.add('loading');
    }, { priority: 'user-blocking' });

    // Prioridad media: Funcionalidad principal
    await scheduler.postTask(() => {
        processFormData();
    }, { priority: 'user-visible' });

    // Baja prioridad: Analytics, logging
    scheduler.postTask(() => {
        trackEvent('form_submitted');
    }, { priority: 'background' });
}

3. Difiere Scripts No Críticos

Mueve scripts que no necesitan ejecutarse durante la carga de página:

<!-- ❌ Scripts bloqueantes durante interacción -->
<script src="heavy-analytics.js"></script>

<!-- ✅ Scripts no críticos diferidos -->
<script src="heavy-analytics.js" defer></script>

<!-- ✅ Cargar después de interacción -->
<script>
document.addEventListener('click', () => {
    import('./non-critical-module.js');
}, { once: true });
</script>

Optimizando el Tiempo de Procesamiento

El tiempo de procesamiento es la duración de tus manejadores de eventos.

1. Mantén los Manejadores de Eventos Ligeros

// ❌ Trabajo síncrono pesado en manejador
button.addEventListener('click', () => {
    const data = fetchDataSync(); // 100ms
    processData(data);            // 100ms
    updateUI(data);               // 50ms
    // Total: 250ms bloqueando
});

// ✅ Retroalimentación inmediata, procesamiento diferido
button.addEventListener('click', async () => {
    // Retroalimentación visual inmediata (< 10ms)
    button.disabled = true;
    button.textContent = 'Cargando...';

    // Cede para permitir pintado
    await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));

    // Luego hacer trabajo pesado
    const data = await fetchData();
    processData(data);
    updateUI(data);
});

2. Usa Web Workers para Cómputo Pesado

Mueve el trabajo intensivo de CPU fuera del hilo principal:

// main.js
const worker = new Worker('computation-worker.js');

button.addEventListener('click', () => {
    // Retroalimentación inmediata
    showLoadingState();

    // Envía trabajo al worker
    worker.postMessage({ type: 'PROCESS', data: largeDataset });
});

worker.onmessage = (e) => {
    if (e.data.type === 'RESULT') {
        hideLoadingState();
        displayResults(e.data.result);
    }
};

// computation-worker.js
self.onmessage = (e) => {
    if (e.data.type === 'PROCESS') {
        // Cómputo pesado ejecuta fuera del hilo principal
        const result = heavyCalculation(e.data.data);
        self.postMessage({ type: 'RESULT', result });
    }
};

3. Virtualiza Listas Largas

No renderices miles de nodos DOM:

// ❌ Renderizando 10,000 items
function renderList(items) {
    items.forEach(item => {
        container.appendChild(createItemElement(item));
    });
}

// ✅ Scroll virtual - solo renderiza items visibles
class VirtualList {
    constructor(container, items, itemHeight) {
        this.container = container;
        this.items = items;
        this.itemHeight = itemHeight;
        this.visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2;

        this.render();
        container.addEventListener('scroll', () => this.render());
    }

    render() {
        const scrollTop = this.container.scrollTop;
        const startIndex = Math.floor(scrollTop / this.itemHeight);
        const endIndex = Math.min(startIndex + this.visibleCount, this.items.length);

        // Solo renderiza items visibles
        this.container.innerHTML = '';
        for (let i = startIndex; i < endIndex; i++) {
            const el = createItemElement(this.items[i]);
            el.style.position = 'absolute';
            el.style.top = `${i * this.itemHeight}px`;
            this.container.appendChild(el);
        }
    }
}

4. Debounce y Throttle en Manejadores de Entrada

// ❌ Procesando cada pulsación de tecla
input.addEventListener('input', (e) => {
    expensiveSearch(e.target.value);
});

// ✅ Con debounce - espera a que el usuario deje de escribir
const debouncedSearch = debounce((query) => {
    expensiveSearch(query);
}, 300);

input.addEventListener('input', (e) => {
    debouncedSearch(e.target.value);
});

// Utilidad de debounce
function debounce(fn, delay) {
    let timeoutId;
    return (...args) => {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => fn(...args), delay);
    };
}

Reduciendo el Retraso de Presentación

El retraso de presentación es el tiempo desde que los manejadores completan hasta que el navegador pinta.

1. Minimiza el Layout Thrashing

// ❌ Layout thrashing - lectura/escritura intercaladas
elements.forEach(el => {
    const width = el.offsetWidth;         // Lee (fuerza layout)
    el.style.width = (width + 10) + 'px'; // Escribe (invalida layout)
});

// ✅ Agrupa lecturas, luego agrupa escrituras
const widths = elements.map(el => el.offsetWidth); // Todas las lecturas primero

elements.forEach((el, i) => {
    el.style.width = (widths[i] + 10) + 'px';      // Todas las escrituras después
});

2. Reduce el Tamaño del DOM

DOMs grandes ralentizan el renderizado:

Impacto del Tamaño del DOM en INP:
├── < 1,500 nodos: Óptimo
├── 1,500 - 5,000: Aceptable
├── 5,000 - 10,000: Impacto en rendimiento
└── > 10,000: Retrasos significativos

Recomendaciones:
- Elimina elementos ocultos del DOM
- Usa scroll virtual para listas largas
- Carga perezosamente contenido fuera de pantalla
- Usa content-visibility: auto

3. Usa CSS Containment

Limita el alcance de las actualizaciones de renderizado:

/* Aísla el renderizado del componente */
.interactive-component {
    contain: layout style paint;
}

/* Para componentes complejos */
.complex-widget {
    contain: strict; /* Más restrictivo */
    content-visibility: auto;
    contain-intrinsic-size: 200px 300px;
}

4. Evita Layouts Síncronos Forzados

// ❌ Forzando cálculo de layout
button.addEventListener('click', () => {
    element.style.width = '100px';
    console.log(element.offsetWidth); // Fuerza layout síncrono
    element.style.height = '50px';
});

// ✅ Agrupa cambios de estilo, lee después
button.addEventListener('click', () => {
    element.style.width = '100px';
    element.style.height = '50px';

    // Lee en el siguiente frame si es necesario
    requestAnimationFrame(() => {
        console.log(element.offsetWidth);
    });
});

Gestión de Scripts de Terceros

Los scripts de terceros frecuentemente perjudican INP al bloquear el hilo principal.

1. Carga Scripts de Terceros Asíncronamente

<!-- ❌ Script de terceros bloqueante -->
<script src="https://cdn.example.com/widget.js"></script>

<!-- ✅ Carga asíncrona -->
<script src="https://cdn.example.com/widget.js" async></script>

<!-- ✅ Carga diferida -->
<script src="https://cdn.example.com/analytics.js" defer></script>

2. Usa Facades para Embeds Pesados

<!-- En lugar de cargar el iframe de YouTube inmediatamente -->
<div class="youtube-facade"
     data-video-id="dQw4w9WgXcQ"
     onclick="loadYouTube(this)">
    <img src="/youtube-thumbnail.jpg" alt="Video">
    <button class="play-button">▶️</button>
</div>

<script>
function loadYouTube(facade) {
    const iframe = document.createElement('iframe');
    iframe.src = `https://www.youtube.com/embed/${facade.dataset.videoId}?autoplay=1`;
    iframe.allow = 'autoplay; encrypted-media';
    facade.replaceWith(iframe);
}
</script>

3. Usa Partytown para Scripts de Terceros

<!-- Ejecuta scripts de terceros en Web Worker -->
<script src="partytown.js"></script>
<script type="text/partytown">
    // Este código de analytics ejecuta fuera del hilo principal
    analytics.track('page_view');
</script>

Monitoreo de INP en Producción

Configuración de Real User Monitoring

import { onINP } from 'web-vitals';

function sendToAnalytics(metric) {
    const body = JSON.stringify({
        name: metric.name,
        value: metric.value,
        id: metric.id,
        page: window.location.pathname,
        // Atribución específica de INP
        eventType: metric.attribution?.eventType,
        eventTarget: metric.attribution?.eventTarget,
        inputDelay: metric.attribution?.inputDelay,
        processingDuration: metric.attribution?.processingDuration
    });

    if (navigator.sendBeacon) {
        navigator.sendBeacon('/analytics', body);
    }
}

onINP(sendToAnalytics);

Configurando Alertas

onINP((metric) => {
    // Alerta por INP pobre
    if (metric.value > 500) {
        console.error('INP pobre detectado:', metric);
        // Enviar a tracking de errores
        errorTracking.captureMessage('INP Pobre', {
            value: metric.value,
            threshold: 500,
            page: window.location.href
        });
    }
});

Lista de Verificación de Optimización de INP

Reducción de Retraso de Entrada
├── [ ] Tareas largas divididas en chunks < 50ms
├── [ ] Usando scheduler.yield() para ceder
├── [ ] Scripts no críticos diferidos
├── [ ] Scripts de terceros cargados async
└── [ ] Hilo principal no bloqueado durante carga

Optimización de Tiempo de Procesamiento
├── [ ] Manejadores de eventos son ligeros
├── [ ] Cómputo pesado en Web Workers
├── [ ] Listas largas virtualizadas
├── [ ] Manejadores de entrada con debounce/throttle
└── [ ] Retroalimentación visual inmediata proporcionada

Reducción de Retraso de Presentación
├── [ ] Sin layout thrashing
├── [ ] Tamaño del DOM bajo 5,000 nodos
├── [ ] CSS containment usado
├── [ ] Sin layouts síncronos forzados
└── [ ] Selectores CSS eficientes

Monitoreo
├── [ ] Librería Web Vitals rastreando INP
├── [ ] Datos de atribución recolectados
├── [ ] Alertas por INP pobre
├── [ ] Profiling regular con DevTools

Artículos Relacionados

Referencias

Este artículo fue mejorado usando fuentes autoritativas:

[1] web.dev: Optimize Interaction to Next Paint https://web.dev/articles/optimize-inp Guía completa de optimización de INP de Google cubriendo las tres fases de latencia de interacción, técnicas específicas para cada fase, y APIs modernas del navegador para mejorar la capacidad de respuesta.

[2] web.dev: Long Tasks and Long Animation Frames https://web.dev/articles/long-tasks-devtools Documentación sobre identificar tareas largas (>50ms) usando Chrome DevTools y Long Animation Frames API. Esencial para encontrar qué está bloqueando el hilo principal y perjudicando INP.

[3] MDN Web Docs: Web Workers API https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API Referencia técnica para la API de Web Workers, permitiendo que cómputo pesado ejecute fuera del hilo principal. Crítico para mantener el hilo principal responsivo a interacciones del usuario.

[4] Chrome Developers: Scheduler API https://developer.chrome.com/docs/web-platform/scheduler Documentación sobre scheduler.yield(), scheduler.postTask(), y priority hints. APIs modernas para ceder explícitamente al hilo principal y programar trabajo por prioridad.


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 INP), MDN Web Docs, Chrome Developers

Artículos relacionados

Versión relacionada

Introducción

Inp Explained

Interaction to Next Paint (INP) 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

Cls Optimization Guide

Cumulative Layout Shift (CLS) mide la estabilidad visual de tu página

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