Development12 min read0 visualizaciones

    INP en 2026: el Core Web Vital que mide lo que pasa después de cargar

    INP mide la responsividad real de tu web. Cuando relevó a FID en 2024, los orígenes con Core Web Vitals buenos cayeron del 46,9 % al 43,4 %.

    Roberto Serrano

    Roberto Serrano

    Ingeniero de Software

    ¿Te resulta útil? Déjame saber con un like o guárdalo para luego.

    El 90 % del tiempo ocurre después de que la página carga

    Durante años optimizamos la carga. LCP, FCP, TTFB, el tamaño del bundle. Tiene sentido: es lo que Lighthouse puntúa y lo que se puede medir sin usuarios reales. Pero hay un dato que cambia la perspectiva, y está en la documentación oficial de INP: el 90 % del tiempo que un usuario pasa en una página transcurre después de que esa página haya cargado.

    Todo lo que ocurre ahí —click en el menú, escribir en el buscador, añadir al carrito— no lo mide ninguna métrica de carga. Lo mide Interaction to Next Paint (INP).

    INP evalúa la responsividad observando la latencia de todas las interacciones de click, tap y teclado durante toda la vida de la visita. El valor final es la interacción más larga, descartando outliers, medida en el percentil 75 de las visitas. No es una media.

    Esa definición tiene una consecuencia incómoda: un solo handler lento en un solo componente puede hundir la métrica de toda la página.

    La historia real: cuando INP sustituyó a FID

    En marzo de 2024, INP reemplazó a First Input Delay como Core Web Vital. FID medía solo el retardo hasta el primer handler de la primera interacción; era fácil de aprobar y decía poco sobre cómo se sentía la página. INP mide todo, todo el tiempo, y en la fase de presentación también.

    Las release notes de CrUX dejaron constancia del efecto sobre un dataset de 18.383.755 orígenes:

    • 46,9 % de orígenes tenían Core Web Vitals buenos con FID.
    • 43,4 % de orígenes tenían Core Web Vitals buenos al usar INP en lugar de FID.

    Al cambiar la métrica, la tasa de aprobados cayó 3,5 puntos: unos 640.000 sitios que llevaban años diciendo "mi web es rápida" descubrieron que no. No porque su web se hubiera vuelto más lenta, sino porque el estándar empezó a medir algo que antes nadie miraba.

    Fija la fecha: esos números son de 2023, antes del cambio oficial, no el estado de 2026. Se citan porque son la única foto pública del momento del relevo. La situación ha mejorado, pero INP sigue siendo el CWV que más sitios suspenden.

    Y hay un error de lectura que circula mucho: el famoso "el 43 % de los sitios falla INP". Es al revés. El 43,4 % era la tasa de aprobados. El recorte de la frase original —"have good Core Web Vitals when using INP in place of FID"— es lo que produce el error de signo.

    Los umbrales y cómo se calcula

    Los umbrales oficiales, medidos a p75 y segmentados por móvil y escritorio:

    ValorCategoría
    ≤ 200 msBueno
    200–500 msNecesita mejorar
    Más de 500 msMalo

    La parte que casi nadie explica bien es el cálculo. Se observan todas las interacciones. Para páginas con muchas, se ignora una de las peores por cada 50 —así se descarta el hipo aleatorio—. El valor final es el p75 de todas las page views.

    La latencia de una interacción es el evento más largo del grupo, desde que el usuario interactúa hasta el siguiente frame pintado. No es la media ni la suma de los handlers. Si un click dispara tres handlers y uno tarda 300 ms, la interacción son esos 300 ms.

    No cuentan el scroll, el hover, el zoom ni el arrastre: un sitio con scroll a 20 fps puede tener INP verde. Fluidez y responsividad no son lo mismo.

    Las tres fases de una interacción

    Toda interacción se descompone en tres tramos, y saber cuál domina es el diagnóstico entero:

    FaseQué esCausa típica
    Input delayDel input hasta que arranca el primer handlerLong tasks previas, hidratación, JS de terceros
    Processing durationEjecución de todos los handlers del frameJS pesado, render de frameworks, layout síncrono
    Presentation delayDe fin de handlers hasta el paintEstilos costosos, layout thrashing, composición

    Si el input delay domina, el problema no está en tu handler: está en lo que el main thread hacía justo antes. Si domina el processing, el handler es caro. Si domina la presentación, el problema son los estilos y el layout.

    Cómo medirlo: campo y laboratorio

    CrUX solo mide Chrome

    El campo de CrUX se alimenta de usuarios reales de Chrome. La documentación lo dice con claridad: las métricas se apoyan en APIs del navegador, y aunque todas existen en Chrome, otros navegadores pueden no soportar el conjunto completo. La Event Timing API, base de INP, tiene soporte parcial o nulo en Safari y Firefox.

    La consecuencia práctica es importante: si tu RUM usa web-vitals en todos los navegadores, en Safari y Firefox obtendrás muchos menos samples de INP, o ninguno. Eso no significa que tu web sea rápida ahí: significa que no lo sabes. Reporta siempre el navegador junto al sample y no mezcles percentiles de navegadores distintos en el mismo dashboard.

    RUM con web-vitals y attribution

    La vía correcta para medir en producción:

    import { onINP } from 'web-vitals/attribution'
    
    onINP(({ value, rating, attribution }) => {
    	navigator.sendBeacon(
    		'/rum',
    		JSON.stringify({
    			name: 'INP',
    			value,
    			rating,
    			target: attribution.interactionTarget,
    			inputDelay: attribution.inputDelay,
    			processing: attribution.processingDuration,
    			presentation: attribution.presentationDelay,
    		}),
    	)
    })
    

    Ese attribution es lo que convierte la métrica en una herramienta de trabajo: te da el elemento que provocó la peor interacción y el desglose por fases. Sin él, solo sabes que hay un problema.

    Los cuatro gotchas oficiales

    La documentación de web-vitals marca cuatro trampas que distorsionan los datos:

    1. Entries por debajo de 104 ms no se reportan por defecto en PerformanceObserver. Se puede bajar con durationThreshold, pero el mínimo es 16 ms. Por eso la librería también observa first-input, que siempre es observable.
    2. bfcache: si la página se restaura desde la caché de ida y vuelta, el INP debe resetearse a 0. Es una visita distinta para el usuario, no una continuación.
    3. iframes: la API no reporta interacciones dentro de iframes, pero la métrica sí las cuenta, porque el usuario no distingue qué es un iframe. Consecuencia directa: un embed de vídeo o un chat de terceros puede ser tu peor INP y no aparecer en tu RUM. CrUX y tu analítica no cuadrarán, y no es un bug.
    4. Pestañas abiertas durante días: en móvil no se ejecutan callbacks de unload en segundo plano. Hay que reportar en visibilitychange y calcular el INP final en el backend. Este es el que más se ignora y el que más deforma los números.

    Laboratorio: DevTools no mide INP, mide TBT

    En el panel Performance hay que grabar con interacción, no solo la carga. Un perfil sin clicks no tiene INP. El carril Interactions muestra la duración de cada una, y el insight de desglose separa input delay, processing y presentation.

    Dos advertencias. La primera: activa CPU throttling de 4x o 6x, porque tu portátil no se parece al móvil del usuario. La segunda: TBT es un proxy razonable de INP, pero no es un sustituto, y la documentación es literal en esto. Un Lighthouse verde con INP rojo en campo es perfectamente posible.

    Cómo arreglarlo, con código

    Ceder el hilo: scheduler.yield() y su fallback

    scheduler.yield() cede el main thread explícitamente para que el navegador pueda pintar y atender input. Su ventaja frente a setTimeout(fn, 0) es que conserva la prioridad de la tarea original: con setTimeout la continuación baja a la cola normal y puede acabar detrás de otro trabajo, incluso detrás de input nuevo, que es justo lo que querías evitar.

    Pero hay que decirlo claro: no es universal. Es Chromium. Si lo presentas como solución mágica, rompes el flujo en el resto de navegadores. Siempre con fallback:

    const yieldToMain = () =>
    	typeof scheduler !== 'undefined' && scheduler.yield
    		? scheduler.yield()
    		: new Promise(resolve => setTimeout(resolve, 0))
    

    Y el patrón de uso, cediendo solo cuando hay input esperando:

    async function processItems(items) {
    	for (const item of items) {
    		render(item)
    		if (navigator.scheduling?.isInputPending?.()) {
    			await yieldToMain()
    		}
    	}
    }
    

    Partir el trabajo en trozos

    Un long task es cualquier tarea de más de 50 ms en el main thread, y es la causa raíz de la mayoría de INP pobres. El patrón es ceder entre unidades de trabajo, no al final:

    async function chunkedWork(items, chunkSize = 50) {
    	for (let i = 0; i < items.length; i += chunkSize) {
    		items.slice(i, i + chunkSize).forEach(doWork)
    		await yieldToMain()
    	}
    }
    

    Para parsing o cálculo pesado que no necesita tocar el DOM, la solución no es ceder: es moverlo a un Web Worker y sacarlo del hilo por completo.

    La técnica oficial: rAF más setTimeout

    Hay un patrón que la documentación de web.dev recomienda explícitamente y que funciona en todos los navegadores, a diferencia de scheduler.yield():

    textBox.addEventListener('input', inputEvent => {
    	// 1. Solo lo imprescindible para el próximo frame
    	updateTextBox(inputEvent)
    
    	// 2. Todo lo demás, después del paint
    	requestAnimationFrame(() => {
    		setTimeout(() => {
    			const text = textBox.textContent
    			updateWordCount(text)
    			checkSpelling(text)
    			saveChanges(text)
    		}, 0)
    	})
    })
    

    El setTimeout(0) solo puede ejecutarse antes de que el navegador pinte; metido dentro de un rAF se garantiza que el frame ya se presentó. Es esotérico, pero efectivo y cross-browser.

    El ejercicio mental vale para cualquier handler: en ese editor hay cuatro tareas por tecla —pintar, contar palabras, corregir, guardar— y solo la primera necesita estar en el frame.

    React: useDeferredValue y startTransition

    El patrón número uno de INP malo en React es el input controlado que re-renderiza un árbol grande en cada tecla:

    import { useState, useDeferredValue, useMemo } from 'react'
    
    function Search({ allItems }) {
    	const [query, setQuery] = useState('')
    	const deferredQuery = useDeferredValue(query)
    
    	const results = useMemo(() => expensiveFilter(allItems, deferredQuery), [deferredQuery])
    
    	return (
    		<>
    			<input value={query} onChange={e => setQuery(e.target.value)} />
    			<Results items={results} stale={query !== deferredQuery} />
    		</>
    	)
    }
    

    query actualiza el input en el mismo frame, así que la tecla responde. deferredQuery se actualiza en un render de baja prioridad, que React puede interrumpir si llega input nuevo.

    La advertencia: useDeferredValue no hace la operación más rápida, la hace interrumpible. Si el render diferido tarda 400 ms, sigues teniendo un long task de 400 ms; lo que cambia es que ya no bloquea la tecla. Con startTransition pasa lo mismo, aplicado a estado que no es urgente.

    Cuando los handlers son rápidos y aun así falla

    Es la parte menos obvia: el processing es corto y el presentation delay se come la interacción. Tres causas habituales.

    Layout thrashing. Actualizas estilos y los lees en la misma tarea, y fuerzas un layout síncrono que el navegador podría haber diferido. En DevTools aparece como Recalculate Style o Layout con el triángulo rojo. Agrupa lecturas y escrituras de DOM en pasadas separadas.

    DOM demasiado grande. El trabajo de render no escala linealmente con el número de nodos, pero un DOM grande encarece cada actualización. Aplana la estructura y usa content-visibility: auto para lo que queda fuera del viewport.

    HTML generado por JavaScript. Cuando el servidor manda HTML en stream, el navegador cede el hilo al parsear por trozos, y eso es gratis. Cuando generas el HTML con JS en cliente, pagas el JS y el navegador no cede hasta acabar de parsear y renderizar. Es una de las razones menos conocidas por las que una SPA equivalente tiene peor INP que un MPA.

    Y una regla de composición: anima transform y opacity, nunca top o width. Cuidado con backdrop-filter y filter: blur() en elementos que se animan al interactuar.

    Cómo encontrar tu interacción culpable

    Este es el núcleo del asunto: CrUX te dice que hay un problema, no cuál es.

    Paso 1: attribution en RUM. El desglose por fases te dice dónde mirar. Si domina inputDelay, busca una long task previa: hidratación, un script de terceros, un trabajo de arranque. Si domina processingDuration, el handler es caro. Si domina presentationDelay, son estilos, layout o paint.

    Paso 2: Long Animation Frames. Si el input delay o el processing dominan, las entries de long-animation-frame te dan el script y la función que bloquearon, no solo "un long task":

    new PerformanceObserver(list => {
    	for (const entry of list.getEntries()) {
    		console.log(
    			entry.duration,
    			entry.scripts.map(s => ({
    				source: s.sourceURL,
    				fn: s.sourceFunctionName,
    				duration: s.duration,
    			})),
    		)
    	}
    }).observe({ type: 'long-animation-frame', buffered: true })
    

    LoAF es la evolución de longtask y lo que se usa hoy para atribución fina.

    Paso 3: reproducir en laboratorio. Móvil de gama media, CPU a 4x, red 4G. E interactuar durante la carga: abrir el menú, cambiar de tab, desplegar un acordeón mientras la página se hidrata. Ahí está el input delay que no aparece si esperas a que cargue todo.

    Paso 4: ir al handler concreto. El interactionTarget te dice que el INP viene del botón de añadir al carrito. No optimices la home entera: optimiza ese handler.

    Lo que hay que recordar

    INP mide la peor interacción, no la media, y su desglose en tres fases te dice el culpable. scheduler.yield() mejora los bucles de render porque preserva prioridad, pero necesita fallback; el patrón rAF más setTimeout funciona en todos los navegadores. CrUX y tu RUM nunca van a cuadrar exactamente, y el laboratorio no mide INP: mide TBT.

    La conclusión práctica es que INP no se arregla con un plugin ni con una auditoría genérica, sino mirando handlers concretos con el elemento identificado y la fase medida.

    Artículos relacionados

    Artículo escrito por Roberto Serrano — Desarrollador web, automatizaciones e IA en Tarragona, España.

    ¿Te ha gustado? Compártelo.

    Más sobre el tema