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
- Abre DevTools → pestaña Performance
- Habilita la casilla “Web Vitals”
- Graba mientras interactúas con la página
- 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
- INP Explicado - Fundamentos de INP
- Guía de Optimización de LCP - Optimizar el pintado más grande
- Guía de Rendimiento de JavaScript - Optimización de JS
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