Cómo programar eventos de dataLayer para optimizar el INP
Cómo posponer los eventos de GTM hasta después del pintado mejora las puntuaciones de INP
TL;DR: Cómo mejorar el INP al posponer dataLayer.push()
Cuando dataLayer.push() se ejecuta de forma síncrona dentro de un manejador de eventos, GTM procesa las etiquetas antes de que el navegador pueda pintar la respuesta visual. Esto bloquea el main thread y dispara el Interaction to Next Paint (INP). La solución: programa dataLayer.push() con requestAnimationFrame para que se ejecute después de que el navegador haya pintado el siguiente frame. Este patrón reduce el INP entre 20 y 100 ms en la mayoría de los sitios, y suele convertir un suspenso en un aprobado. La recogida de datos se retrasa entre 50 y 250 ms, algo irrelevante para la analítica y el tracking de marketing.
Cómo perjudica dataLayer.push() al INP
En uno de nuestros clientes, medimos una reducción de 100 ms en el Interaction to Next Paint (INP) al reprogramar la ejecución de dataLayer.push() tras una interacción del usuario. La solución es un reemplazo directo en JavaScript que prioriza el renderizado frente al tracking.
Última revisión por Arjen Karel en marzo de 2026
Table of Contents!
- TL;DR: Cómo mejorar el INP al posponer dataLayer.push()
- Cómo perjudica dataLayer.push() al INP
- El problema del dataLayer.push() síncrono
- La solución: pintar primero y hacer push después
- El helper awaitPaint
- Override global para pruebas rápidas
- Por qué esto mejora el INP
- ¿Por qué no un retardo fijo, Idle Callback o scheduler.yield()?
- Trade-offs
El problema del dataLayer.push() síncrono
Si trabajas con Google Tag Manager, ya conoces dataLayer.push(). Es el método estándar para enviar eventos al Data Layer y está integrado en prácticamente todas las funciones del sitio. El problema es que nadie se cuestiona cuándo se ejecuta.
Al llamar a dataLayer.push() directamente dentro de un manejador de eventos (un clic en un botón, el envío de un formulario o el despliegue de un menú), este se ejecuta de forma síncrona. Las etiquetas de GTM configuradas para dispararse con ese evento también se ejecutan de inmediato, bloqueando el main thread antes de que el navegador pueda actualizar la interfaz. El visitante hace clic en un botón y no ve nada en pantalla mientras GTM procesa scripts de tracking en segundo plano. Ese retraso es el presentation delay, y es el principal responsable de un INP deficiente.
La traza de rendimiento de abajo, correspondiente a un importante sitio web de noticias, muestra la actividad de GTM tras una interacción del usuario. Las tareas de GTM tardaron aproximadamente 90 ms en ejecutarse, elevando el INP global a 263 ms. Esa interacción suspende los Core Web Vitals.

La solución: pintar primero y hacer push después
Esta solución sigue las recomendaciones de optimización de INP de web.dev: hacer yield al main thread para que el navegador pueda pintar antes de ejecutar código no crítico. En lugar de ejecutar el tracking de forma síncrona dentro del manejador de eventos:
- Ejecuta el código de la actualización visual de inmediato.
- Deja que el navegador pinte.
- Haz el push al dataLayer.
Así se ve la misma interacción tras aplicar este patrón. El único cambio es que dataLayer.push() ahora se ejecuta después de que el navegador haya pintado el siguiente frame.

La interacción que antes suspendía ahora aprueba con holgura. Todos los datos siguen llegando al dataLayer. La diferencia es que los scripts de GTM se ejecutan después de que el navegador responda a la acción del usuario, no antes.
El helper awaitPaint
Este helper utiliza requestAnimationFrame para programar un callback después de que el navegador haya pintado. El setTimeout anidado garantiza que la función se ejecute tras completarse el pintado, no justo antes. Este es el patrón recomendado por web.dev para posponer tareas no críticas tras una interacción.
async function awaitPaint(fn) {
await new Promise((resolve) => {
// Fallback: garantiza que no se quede colgado si RAF nunca se dispara (p. ej., en pestañas en segundo plano)
setTimeout(resolve, 200);
// Programar tras el siguiente pintado
requestAnimationFrame(() => {
setTimeout(resolve, 50);
});
});
if (typeof fn === 'function') {
fn();
}
}
Cómo usar el helper
Envuelve cualquier llamada a dataLayer.push() con awaitPaint para posponerla 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 se produce de inmediato
showLoadingSpinner();
// El tracking se dispara después de pintar
pushToDataLayer('purchase_click', { productId: '123' });
});
Override global para pruebas rápidas
Para probar este patrón en todo el sitio sin tener que refactorizar cada llamada a dataLayer.push(), puedes sobrescribir la función push de forma global. Coloca este script en el <head> justo después del snippet del contenedor de GTM.
<script type="module">
window.dataLayer = window.dataLayer || [];
async function awaitPaint(fn) {
await new Promise((resolve) => {
// Fallback: garantiza que no se quede colgado si RAF nunca se dispara (p. ej., en pestañas en segundo plano)
setTimeout(resolve, 200);
// Programar tras el siguiente pintado
requestAnimationFrame(() => {
setTimeout(resolve, 50);
});
});
if (typeof fn === 'function') {
fn();
}
}
if (window.dataLayer && typeof window.dataLayer.push === 'function') {
const originalPush = window.dataLayer.push.bind(window.dataLayer);
window.dataLayer.push = function (...args) {
awaitPaint(() => {
originalPush(...args);
});
};
}
</script>
Esto sobrescribe cada dataLayer.push() de la página. Todos los triggers de eventos de GTM existentes, las llamadas de tracking hardcodeadas y las integraciones de plugins se benefician automáticamente sin tener que modificar el código.
Por qué esto mejora el INP
El INP mide el tiempo transcurrido desde la interacción de un usuario hasta que el navegador pinta la respuesta visual. Si ejecutas el procesamiento de eventos de GTM de forma síncrona dentro de un manejador de eventos, bloqueas el main thread e impides el renderizado. El Web Almanac de 2024 constató que el presentation delay es el principal responsable de un INP deficiente en la mediana, y los scripts de tracking están entre las causas principales.
Los datos 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 consigue. Esos sitios líderes cargan más scripts de terceros. En Subito (el mayor portal de clasificados de Italia), desactivar un único script de tracking de TikTok cargado mediante GTM redujo el INP de 208 ms a unos 170 ms. Un solo script, 38 ms de mejora.
Al posponer dataLayer.push() hasta después del pintado, sacas todo el procesamiento de GTM de la ruta crítica de la interacción. El usuario recibe respuesta visual de inmediato. GTM se sigue disparando, solo que entre 50 y 250 ms más tarde.
¿Por qué no un retardo fijo, Idle Callback o scheduler.yield()?
setTimeout(delay)fijo: Un retardo fijo (p. ej.,setTimeout(..., 100)) implica adivinar cuándo terminará el renderizado. Si te pasas de largo, retrasas el tracking sin necesidad. Si te quedas corto, sigues bloqueando el pintado.requestIdleCallback: Programa tareas para cuando el navegador esté inactivo, pero no garantiza una ejecución rápida tras una interacción concreta. El callback podría ejecutarse mucho más tarde o ni siquiera llegar a ejecutarse antes de que el usuario cambie de página.scheduler.yield(): La API moderna para hacer yield al main thread. Mantiene la prioridad de las tareas (a diferencia desetTimeout, que envía la continuación al final de la cola). Sin embargo,scheduler.yield()no garantiza que se haya producido un pintado antes de que se ejecute el código. Para este caso concreto,requestAnimationFramees mejor opción porque está vinculado al ciclo de vida del renderizado. Ten en cuenta quescheduler.yield()todavía 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 una vez completado ese pintado. En conjunto, garantizan que el código pospuesto se ejecute después de la actualización visual, que es justo lo que mide el INP.
Trade-offs
- Microrretraso en la recopilación de datos: Los eventos llegan a GTM entre 50 y 250 ms más tarde que con un push síncrono. Para la analítica y el tracking de marketing, esto es irrelevante.
- Pérdida de datos en salidas rápidas: Si un visitante activa un evento y abandona la página dentro de esa ventana de retardo, puede que el evento no llegue a dispararse. Para eventos críticos antes de una redirección o del cierre de la página, usa
navigator.sendBeacon()en lugar dedataLayer.push(). Consulta los motivos para limitar los scripts de analítica para saber más sobre la Beacon API.
Para la mayoría del tracking, la mejora en el INP compensa con creces este retraso mínimo. Si necesitas precisión de milisegundos en el registro de eventos (pujas en tiempo real, dashboards financieros), este enfoque no es el adecuado. Para todo lo demás, la ventaja es indiscutible.
Tras aplicar el override, verifica la mejora con Real User Monitoring. Los tests con lab data mostrarán la diferencia, pero el field data de usuarios reales en redes reales es lo que cuenta para las puntuaciones de Core Web Vitals.
Entérate de qué va lento de verdad.
Trazo tu critical rendering path con datos reales. Te paso una lista de fixes priorizada. No otro informe de Lighthouse.
Quiero la auditoría