Programación de eventos dataLayer para optimizar el INP

Aplazamiento de eventos GTM hasta que el diseño se estabilice para mejorar los valores de INP

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2025-07-14

TL;DR: Mejorando el INP optimizando Google Tag Manager

El problema: Las llamadas estándar de Google Tag Manager (dataLayer.push()), especialmente cuando se activan inmediatamente por interacciones del usuario (como clics o toques), pueden retrasar la capacidad del navegador para mostrar actualizaciones visuales. Esto impacta negativamente la puntuación de Interaction to Next Paint (INP) porque el navegador se ve obligado a procesar las tareas de GTM antes de renderizar la retroalimentación visual de esa interacción.

La solución: Podemos diferir estas llamadas a dataLayer.push() hasta después de que el navegador haya pintado el siguiente frame. Esto prioriza el renderizado de la retroalimentación visual inmediata para el usuario. La corrección implica un pequeño fragmento de JavaScript que modifica el comportamiento predeterminado de dataLayer.push() para incorporar este aplazamiento.

El beneficio: Este enfoque comúnmente resulta en una reducción de 20ms a 100ms en el INP para nuestros clientes, a menudo transformando puntuaciones fallidas de Core Web Vitals en aprobadas. Los usuarios experimentan una interfaz notablemente más ágil. Aunque la recolección de datos para GTM se retrasa ligeramente (típicamente 50-250ms), esta es una compensación aceptable para la mayoría de los propósitos de analítica y marketing.

Abordando los desafíos de INP causados por la ejecución de Google Tag Manager

Para uno de nuestros clientes, observamos una reducción de 100ms en su métrica de Interaction to Next Paint (INP) simplemente reprogramando cuándo se ejecuta la función dataLayer.push() después de una interacción del usuario. Esta mejora se logró utilizando un reemplazo "drop-in" de JavaScript fácil de aplicar y probar que prioriza el renderizado.

El problema de INP con dataLayer.push() 

Si has trabajado con Google Tag Manager (GTM), estás familiarizado con dataLayer.push(). Es el método estándar para enviar datos o eventos al Data Layer, permitiendo que las etiquetas se activen. Es ampliamente utilizado, está profundamente integrado en muchas funcionalidades de sitios web, y sus implicaciones de rendimiento rara vez se cuestionan. Sin embargo, asumir que siempre se ejecuta en el momento óptimo para la user experience puede ser problemático.

Cuando dataLayer.push() se llama directamente dentro de un manejador de eventos para una interacción del usuario (por ejemplo, un clic en un botón), típicamente se ejecuta de forma síncrona. Esto significa que cualquier etiqueta de GTM configurada para activarse en base a ese evento también intentará ejecutarse inmediatamente y bloquear el hilo principal antes de una actualización de diseño. Este bloqueo impide que el navegador renderice rápidamente los cambios visuales esperados de la interacción del usuario (por ejemplo, abrir un menú, mostrar un spinner de carga), lo que lleva a una puntuación de INP deficiente.

Examinemos qué sucede. La traza de rendimiento a continuación, de un importante sitio de noticias, destaca la actividad relacionada con GTM después de una interacción del usuario. En este caso, las tareas de GTM tardaron aproximadamente 90ms en ejecutarse y con un valor general de INP de 263ms, ¡esta interacción falla los Core Web Vitals!

datalayer push inp

La solución: ¡priorizar el pintado, luego enviar al datalayer!

La solución es tanto simple como elegante, alineándose con las mejores prácticas de optimización de INP: priorizar la percepción de velocidad del usuario. En lugar de ejecutar todo el código (manejo de interacción, actualizaciones visuales y seguimiento de GTM) de forma síncrona, deberíamos:

  1. Ejecutar el código crítico para la actualización visual inmediatamente.
  2. Permitir que el navegador pinte estos cambios visuales.
  3. Luego, ejecutar código menos crítico, como enviar eventos al dataLayer.

Este enfoque a menudo se llama "yielding to the main thread." Veamos el impacto cuando aplicamos este patrón de yielding a las llamadas de dataLayer.push() en el mismo sitio y para exactamente la misma interacción que antes. La única diferencia es que hemos programado el dataLayer.push() para que ocurra después de que el navegador haya tenido la oportunidad de renderizar el siguiente frame usando requestAnimationFrame.

datalayer push inp yeilded

Como puedes ver, la misma interacción que antes fallaba ahora pasa cómodamente los Core Web Vitals. Todos los datos necesarios aún se envían al dataLayer. La diferencia crucial es que los scripts relacionados con GTM ahora se ejecutan después de que el navegador haya actualizado el diseño en respuesta a la acción del usuario. Esto significa que tu visitante obtiene retroalimentación visual inmediata, mejorando su experiencia, en lugar de esperar a que los scripts de seguimiento se procesen.

Aplicando el código

El código funciona anulando la función predeterminada dataLayer.push() con una función modificada que envía los datos al dataLayer después de que se haya realizado una actualización de diseño.

Función auxiliar Await Paint

Esta función auxiliar usa requestAnimationFrame para programar un callback que se ejecute después de que el navegador haya pintado el siguiente frame.

// --- INP Yield Pattern Implementation ---

// This helper ensures that a function only runs after the next paint (or safe fallback)
async function awaitPaint(fn) {
    await new Promise((resolve) => {
        // Fallback timeout: ensures we don't hang forever if RAF never fires
        setTimeout(resolve, 200); 

        // Request the next animation frame (signals readiness to paint)
        requestAnimationFrame(() => {
            // Small delay to ensure the frame is actually painted, not just queued
            setTimeout(resolve, 50);
        });
    });

    // Once the paint (or fallback) happens, run the provided function
    if (typeof fn === 'function') {
        fn();
    }
}

Ejemplo de implementación

Este es un ejemplo de una función utilitaria de React que programa automáticamente el envío al dataLayer.

export const pushToDataLayer = (event: string, data: Record<string, any> = {}): void => {
  // Ensure dataLayer exists
  if (typeof window !== 'undefined') {
    window.dataLayer = window.dataLayer || [];
    
    // wait for paint
    awaitPaint(() => {
        // Push event and data to dataLayer
        window.dataLayer.push({
          event,
          ...data,
          timestamp: new Date().toISOString()
        });
    });
  }
};

// Usage in a React component:
// import { useState, useEffect } from 'react';
// import { pushToDataLayer } from '../utils/analytics';

// function ProductCard({ product }) {
//   const [isWishlisted, setIsWishlisted] = useState(false);
  
//   // Track wishlist changes
//   useEffect(() => {
//     if (isWishlisted) {
//       pushToDataLayer('addToWishlist', {
//         productId: product.id,
//         productName: product.name,
//         productPrice: product.price
//       });
//     }
//   }, [isWishlisted, product]);
  
//   return (
//     <div className="product-card">
//       <h3>{product.name}</h3>
//       <p>${product.price}</p>
//       <button onClick={() => setIsWishlisted(!isWishlisted)}>
//         {isWishlisted ? '♥' : '♡'} {isWishlisted ? 'Wishlisted' : 'Add to Wishlist'}
//       </button>
//     </div>
//   );
// }

Pruebas simplificadas: anulación global

Para probar rápidamente este patrón en todo tu sitio o para implementaciones existentes de dataLayer.push() sin refactorizar cada una, puedes anular globalmente la función dataLayer.push().

Importante: Coloca este script en la parte superior del <head> de tu HTML, inmediatamente después de que se cargue el script del contenedor de GTM. Esto asegura que tu anulación esté en su lugar lo antes posible.

<script type="module">
    // Ensure dataLayer exists (standard GTM snippet part)
    window.dataLayer = window.dataLayer || [];

    // --- INP Yield Pattern Helper ---
    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);
            });
        });
    }

    // --- Applying the pattern to Google Tag Manager dataLayer.push globally ---
    if (window.dataLayer && typeof window.dataLayer.push === 'function') {
        // Preserve the original push function
        const originalDataLayerPush = window.dataLayer.push.bind(window.dataLayer);

        // Override dataLayer.push
        window.dataLayer.push = function (...args) {
            // Using an IIFE to use async/await syntax if preferred,
            // or directly call awaitPaint.
            (async () => {
                await awaitPaint(() => {
                    // Call the original push with its arguments after yielding to paint
                    originalDataLayerPush(...args);
                });
            })();
            // Return the value the original push would have, if any (though typically undefined)
            // For GTM, the push method doesn't have a meaningful return value for the caller.
            // The primary purpose is the side effect of adding to the queue.
        };
        console.log('dataLayer.push has been overridden to improve INP.');
    }
</script>

Por qué esto ayuda al Interaction to Next Paint

INP mide la latencia desde una interacción del usuario (por ejemplo, clic, toque, pulsación de tecla) hasta que el navegador pinta la siguiente actualización visual en respuesta a esa interacción. Si ejecutas de forma síncrona tareas que consumen muchos recursos, como el procesamiento de eventos de GTM y la activación de etiquetas, inmediatamente después de una interacción, bloqueas el hilo principal del navegador. Esto impide el renderizado de la retroalimentación visual que el usuario espera. Al diferir la ejecución de JavaScript no crítico como el seguimiento de GTM hasta después de que el navegador haya pintado las actualizaciones visuales, este patrón asegura que los usuarios reciban retroalimentación visual rápida, mejorando significativamente la puntuación de INP.

¿Por qué no usar simplemente un retraso fijo, idle Callback o Scheduler?

  • setTimeout(delay) fijo: Usar un retraso codificado (por ejemplo, setTimeout(..., 100)) es esencialmente adivinar cuándo se completará el renderizado. No es adaptativo; podría ser demasiado largo (retrasando el seguimiento innecesariamente) o demasiado corto (aún bloqueando el pintado).
  • requestIdleCallback: Esta API programa trabajo cuando el navegador está inactivo. Aunque es útil para tareas en segundo plano, no garantiza la ejecución rápidamente después de la actualización visual de una interacción específica. El callback podría ejecutarse mucho después o, durante períodos ocupados, no ejecutarse en absoluto antes de que el usuario navegue fuera.
  • Schedulers genéricos (postTask etc.): Aunque el scheduler postTask del navegador ofrece priorización, requestAnimationFrame está específicamente vinculado al ciclo de vida del renderizado. La función auxiliar awaitPaint aprovecha esto usando requestAnimationFrame como señal de que el navegador se está preparando para pintar, y luego agrega un retraso mínimo para ejecutarse después de que ese pintado probablemente se haya completado.

Compensaciones

Toda optimización tiene posibles compensaciones.

  • Micro-retraso en la recolección de datos: Esta técnica introduce un micro-retraso predecible (aproximadamente 50-250ms, dependiendo de la carga del navegador y los timeouts específicos usados en awaitPaint) antes de que los datos del evento lleguen a Google Tag Manager.
  • Riesgo de pérdida de datos en salida rápida: Si un visitante activa un evento y luego abandona la página dentro de esta ventana de micro-retraso (antes de que se ejecute el dataLayer.push diferido), los datos de ese evento específico podrían no enviarse. Para eventos críticos donde este riesgo es inaceptable (por ejemplo, inmediatamente antes de una redirección o descarga de página), mecanismos alternativos de seguimiento como navigator.sendBeacon podrían considerarse para esos eventos específicos, aunque esto está fuera del alcance de la anulación de dataLayer.push.

Para la mayoría del seguimiento estándar de analítica y marketing, este ligero retraso es insignificante y una compensación valiosa por la mejora significativa en el rendimiento percibido por el usuario y las puntuaciones de INP. Sin embargo, para escenarios de latencia ultra baja (por ejemplo, algunos tipos de interacciones de pujas en tiempo real directamente vinculadas a eventos de GTM, o dashboards financieros altamente sensibles donde la precisión de milisegundos en el registro de eventos es primordial), este enfoque podría no ser adecuado.

De lo contrario, la ganancia en rendimiento de INP y user experience típicamente supera con creces la latencia mínima en la recolección de datos.De lo contrario, la ganancia en rendimiento de INP y user experience típicamente supera con creces la latencia mínima en la recolección de datos.

Compare your segments.

Is iOS slower than Android? Is the checkout route failing INP? Filter by device, route, and connection type.

Analyze Segments >>

  • Device filtering
  • Route Analysis
  • Connection Types
Programación de eventos dataLayer para optimizar el INPCore Web Vitals Programación de eventos dataLayer para optimizar el INP