Programación de eventos del dataLayer para optimizar el INP
Cómo diferir los eventos de GTM hasta después del pintado del navegador mejora tus puntuaciones de INP

TL;DR: Mejorar el INP difiriendo dataLayer.push()
Cuando dataLayer.push() se ejecuta sincrónicamente dentro de un manejador de eventos, GTM procesa las etiquetas antes de que el navegador pueda pintar la respuesta visual. Esto bloquea el hilo principal e infla tu Interaction to Next Paint (INP). La solución: programa dataLayer.push() para que se ejecute después de que el navegador haya pintado el siguiente frame usando requestAnimationFrame. Este patrón recorta de 20 a 100 ms del INP en la mayoría de los sitios, a menudo convirtiendo una puntuación deficiente en una aprobada. La recopilación de datos se retrasa entre 50 y 250 ms, lo cual es intrascendente para la analítica y el seguimiento de marketing.
Cómo dataLayer.push() perjudica tu INP
Para uno de nuestros clientes, medimos una reducción de 100 ms en el Interaction to Next Paint (INP) al reprogramar cuándo se ejecuta dataLayer.push() después de una interacción del usuario. La solución es un reemplazo directo en JavaScript que prioriza el renderizado sobre el seguimiento.
Última revisión por Arjen Karel en marzo de 2026
Table of Contents!
- TL;DR: Mejorar el INP difiriendo dataLayer.push()
- Cómo dataLayer.push() perjudica tu INP
- El problema con el dataLayer.push() síncrono
- La solución: pintar primero, luego hacer push
- El helper awaitPaint
- Sobrescritura global para pruebas rápidas
- Por qué esto mejora el INP
- ¿Por qué no un retraso fijo, Idle Callback o scheduler.yield()?
- Compensaciones
El problema con el dataLayer.push() síncrono
Si trabajas con Google Tag Manager, conoces dataLayer.push(). Es el método estándar para enviar eventos al Data Layer, y está profundamente integrado en la mayoría de las funcionalidades del sitio. El problema es que nadie se cuestiona cuándo se ejecuta.
Cuando dataLayer.push() se llama directamente dentro de un manejador de eventos (un clic en un botón, el envío de un formulario, el cambio de un menú), se ejecuta sincrónicamente. Las etiquetas de GTM configuradas para activarse con ese evento también se ejecutan inmediatamente, bloqueando el hilo principal antes de que el navegador pueda actualizar el diseño. El visitante hace clic en un botón y no ve que ocurra nada mientras GTM procesa los scripts de seguimiento en segundo plano. Ese retraso es tu presentation delay, y es el principal contribuyente a un INP deficiente.
La traza de rendimiento a continuación, de un importante sitio web de noticias, muestra la actividad de GTM después de una interacción del usuario. Las tareas de GTM tardaron aproximadamente 90 ms en ejecutarse, empujando el INP general a 263 ms. Esa interacción suspende las Core Web Vitals.

La solución: pintar primero, luego hacer push
La solución se alinea con la guía de optimización de INP de web.dev: hacer yield al hilo principal para que el navegador pueda pintar antes de ejecutar código no crítico. En lugar de ejecutar el seguimiento de forma síncrona dentro del manejador de eventos:
- Ejecuta el código para la actualización visual inmediatamente.
- Deja que el navegador pinte.
- Luego haz push al dataLayer.
Aquí está la misma interacción después de aplicar este patrón. El único cambio es que dataLayer.push() ahora se ejecuta después de que el navegador ha pintado el siguiente frame.

El helper awaitPaint
Este helper utiliza requestAnimationFrame para programar un callback después de que el navegador haya pintado. El setTimeout anidado asegura que la función se ejecute después de que se complete el pintado, no justo antes. Este es el patrón recomendado por web.dev para diferir el trabajo no crítico después de una interacción.
async function awaitPaint(fn) {
await new Promise((resolve) => {
// Fallback: asegura que no nos colguemos si RAF nunca se dispara (ej. pestañas en segundo plano)
setTimeout(resolve, 200);
// Programar después del siguiente pintado
requestAnimationFrame(() => {
setTimeout(resolve, 50);
});
});
if (typeof fn === 'function') {
fn();
}
}
Usando el helper
Envuelve cualquier llamada a dataLayer.push() en awaitPaint para diferirla hasta después del pintado:
function pushToDataLayer(event, data) {
window.dataLayer = window.dataLayer || [];
awaitPaint(() => {
window.dataLayer.push({
event,
...data,
timestamp: new Date().toISOString()
});
});
}
// Uso
document.querySelector('.buy-button').addEventListener('click', () => {
// La respuesta visual ocurre inmediatamente
showLoadingSpinner();
// El seguimiento se ejecuta después del pintado
pushToDataLayer('purchase_click', { productId: '123' });
});
Sobrescritura global para pruebas rápidas
Para probar este patrón en todo tu sitio sin refactorizar cada llamada a dataLayer.push(), puedes sobrescribir globalmente la función push. Coloca este script en el <head> inmediatamente después del fragmento del contenedor de GTM.
<script type="module">
window.dataLayer = window.dataLayer || [];
async function awaitPaint(fn) {
return new Promise((resolve) => {
const fallbackTimeout = setTimeout(() => {
if (typeof fn === 'function') { fn(); }
resolve();
}, 200);
requestAnimationFrame(() => {
setTimeout(() => {
clearTimeout(fallbackTimeout);
if (typeof fn === 'function') { fn(); }
resolve();
}, 50);
});
});
}
if (window.dataLayer && typeof window.dataLayer.push === 'function') {
const originalPush = window.dataLayer.push.bind(window.dataLayer);
window.dataLayer.push = function (...args) {
(async () => {
await awaitPaint(() => {
originalPush(...args);
});
})();
};
}
</script>
Esto sobrescribe cada dataLayer.push() en la página. Todos los activadores de eventos de GTM existentes, las llamadas de seguimiento integradas en el código y las integraciones de plugins se benefician automáticamente sin ningún cambio en el código.
Por qué esto mejora el INP
El INP mide el tiempo desde una interacción del usuario hasta que el navegador pinta la respuesta visual. Si ejecutas sincrónicamente el procesamiento de eventos de GTM dentro de un manejador de eventos, bloqueas el hilo principal y evitas el renderizado. El Web Almanac de 2024 descubrió que el presentation delay es el mayor contribuyente a un INP deficiente en la mediana, y los scripts de seguimiento están entre las causas principales.
Los números lo confirman. El Web Almanac de 2025 muestra que el 77% de las páginas móviles aprueban el INP en general, pero solo el 63% de los 1.000 sitios más visitados lo hacen. Esos sitios principales cargan más scripts de terceros. En Subito (el mercado de clasificados más grande de Italia), deshabilitar un solo script de seguimiento de TikTok cargado a través de GTM redujo el INP de 208 ms a aproximadamente 170 ms. Un script, 38 ms ahorrados.
Al diferir dataLayer.push() hasta después del pintado, sacas todo el procesamiento de GTM de la ruta crítica de la interacción. El usuario obtiene una respuesta visual inmediatamente. GTM se sigue activando, solo que de 50 a 250 ms después.
¿Por qué no un retraso fijo, Idle Callback o scheduler.yield()?
setTimeout(delay)fijo: Un retraso predeterminado en el código (por ejemplo,setTimeout(..., 100)) es adivinar cuándo se completará el renderizado. Demasiado largo y retrasas el seguimiento innecesariamente. Demasiado corto y sigues bloqueando el pintado.requestIdleCallback: Programa el trabajo cuando el navegador está inactivo, pero no garantiza la ejecución rápidamente después de una interacción específica. El callback podría ejecutarse mucho más tarde, o no ejecutarse en absoluto antes de que el usuario abandone la página.scheduler.yield(): La API moderna para hacer yield al hilo principal. Preserva la prioridad de la tarea (a diferencia desetTimeout, que envía tu continuación al final de la cola). Sin embargo,scheduler.yield()no garantiza que haya ocurrido un pintado antes de que se ejecute tu código. Para este caso de uso específico,requestAnimationFramees la mejor herramienta porque está ligada al ciclo de vida de renderizado. Ten en cuenta quescheduler.yield()aún no es compatible con Safari.
La combinación de requestAnimationFrame + setTimeout está diseñada específicamente para este escenario. requestAnimationFrame se dispara justo antes de que el navegador pinte. El setTimeout anidado crea una nueva tarea que se ejecuta después de que se completa ese pintado. Juntos garantizan que tu código diferido se ejecute después de la actualización visual, que es exactamente lo que mide el INP.
Compensaciones
- Micro-retraso en la recopilación de datos: Los eventos llegan a GTM de 50 a 250 ms más tarde que con un push síncrono. Para la analítica y el seguimiento de marketing, esto es intrascendente.
- Pérdida de datos por salida rápida: Si un visitante activa un evento y abandona la página dentro de la ventana de retraso, es posible que ese evento no se dispare. Para eventos críticos antes de una redirección o descarga de la página, usa
navigator.sendBeacon()en lugar dedataLayer.push(). Consulta el caso para limitar los scripts de analítica para obtener más información sobre la API Beacon.
Para la mayoría del seguimiento, la ganancia en el INP supera con creces el retraso mínimo. Si necesitas precisión de milisegundos en el registro de eventos (pujas en tiempo real, paneles financieros), este enfoque no es adecuado. Para todo lo demás, es una clara victoria.
Después de aplicar la sobrescritura, verifica la mejora con Real User Monitoring. Las pruebas de laboratorio mostrarán la diferencia, pero los datos de campo de usuarios reales en redes reales son los que cuentan para tus puntuaciones de Core Web Vitals.
I have done this before at your scale.
Complex platforms, large dev teams, legacy code. I join your team as a specialist, run the performance track, and hand it back in a state you can maintain.
Discuss Your Situation
