dataLayer Events plannen om de INP te optimaliseren

GTM events uitstellen tot de layout stabiel is voor betere INP-waarden

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

TL;DR: INP verbeteren door Google Tag Manager te optimaliseren

Het probleem: Standaard Google Tag Manager (dataLayer.push()) aanroepen, vooral wanneer ze direct worden getriggerd door gebruikersinteracties (zoals klikken of tikken), kunnen de mogelijkheid van de browser om visuele updates te tonen vertragen. Dit heeft een negatieve invloed op de Interaction to Next Paint (INP) score, omdat de browser gedwongen wordt GTM-taken te verwerken voordat de visuele feedback voor die interactie wordt gerenderd.

De oplossing: We kunnen deze dataLayer.push() aanroepen uitstellen tot nadat de browser het volgende frame heeft geschilderd. Dit geeft prioriteit aan het renderen van directe visuele feedback voor de gebruiker. De fix omvat een klein JavaScript-snippet dat het standaard dataLayer.push()-gedrag aanpast om dit uitstel te integreren.

Het voordeel: Deze aanpak resulteert doorgaans in een verlaging van 20ms tot 100ms in INP voor onze klanten, en transformeert vaak falende Core Web Vitals-scores naar voldoende. Gebruikers ervaren een merkbaar snellere interface. Hoewel dataverzameling voor GTM iets wordt vertraagd (doorgaans 50-250ms), is dit een acceptabele afweging voor de meeste analytics- en marketingdoeleinden.

INP-uitdagingen aanpakken veroorzaakt door Google Tag Manager-uitvoering

Bij een van onze klanten zagen we een vermindering van 100ms in hun Interaction to Next Paint (INP) metric door simpelweg het moment te herplannen waarop de dataLayer.push()-functie wordt uitgevoerd na een gebruikersinteractie. Deze verbetering werd bereikt met een eenvoudig toe te passen en te testen JavaScript "drop-in" vervanging die prioriteit geeft aan rendering.

Het INP-probleem met dataLayer.push() 

Als je met Google Tag Manager (GTM) hebt gewerkt, ben je bekend met dataLayer.push(). Het is de standaardmethode voor het verzenden van gegevens of events naar de Data Layer, waardoor tags worden geactiveerd. Het wordt veel gebruikt, is diep geïntegreerd in veel sitefunctionaliteiten, en de prestatie-implicaties worden zelden in vraag gesteld. Echter, aannemen dat het altijd op het optimale moment voor user experience wordt uitgevoerd, kan problematisch zijn.

Wanneer dataLayer.push() direct wordt aangeroepen binnen een event handler voor een gebruikersinteractie (bijv. een klik op een knop), wordt het doorgaans synchroon uitgevoerd. Dit betekent dat alle GTM-tags die zijn geconfigureerd om te activeren op basis van dat event, ook onmiddellijk zullen proberen uit te voeren en de main thread blokkeren vóór een layout-update. Deze blokkering voorkomt dat de browser snel de visuele wijzigingen rendert die verwacht worden van de interactie van de gebruiker (bijv. het openen van een menu, het tonen van een laadspinner), wat leidt tot een slechte INP-score.

Laten we bekijken wat er gebeurt. De performance trace hieronder, van een grote nieuwswebsite, toont GTM-gerelateerde activiteit na een gebruikersinteractie. In dit geval duurden de GTM-taken ongeveer 90ms om uit te voeren en met een totale INP-waarde van 263ms faalt deze interactie voor de Core Web Vitals!

De oplossing: geef prioriteit aan paint, en push dan naar de datalayer!

De oplossing is zowel eenvoudig als elegant en sluit aan bij INP-optimalisatie best practices: geef prioriteit aan de snelheidsperceptie van de gebruiker. In plaats van alle code (interactieafhandeling, visuele updates en GTM-tracking) synchroon uit te voeren, moeten we:

  1. De kritieke code voor de visuele update onmiddellijk uitvoeren.
  2. De browser deze visuele wijzigingen laten painten.
  3. Vervolgens minder kritieke code uitvoeren, zoals events naar de dataLayer pushen.

Deze aanpak wordt vaak "yielding to the main thread" genoemd. Laten we de impact bekijken wanneer we dit yielding-patroon toepassen op dataLayer.push()-aanroepen op dezelfde site en voor exact dezelfde interactie als hiervoor. Het enige verschil is dat we de dataLayer.push() hebben gepland om plaats te vinden nadat de browser de kans heeft gehad het volgende frame te renderen met behulp van requestAnimationFrame.

Zoals je kunt zien, slaagt dezelfde interactie die eerder faalde nu comfortabel voor de Core Web Vitals. Alle benodigde gegevens worden nog steeds naar de dataLayer gestuurd. Het cruciale verschil is dat GTM-gerelateerde scripts nu worden uitgevoerd nadat de browser de layout heeft bijgewerkt als reactie op de actie van de gebruiker. Dit betekent dat je bezoeker directe visuele feedback krijgt, wat hun ervaring verbetert, in plaats van te wachten tot trackingscripts zijn verwerkt.

De code toepassen

De code werkt door de standaard dataLayer.push()-functie te overschrijven met een aangepaste functie die de gegevens naar de dataLayer pusht nadat een layout-update is uitgevoerd.

Await Paint helperfunctie

Deze helperfunctie gebruikt requestAnimationFrame om een callback te plannen die wordt uitgevoerd nadat de browser het volgende frame heeft geschilderd.

// --- 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();
    }
}

Implementatievoorbeeld

Dit is een voorbeeld van een React utility-functie die de dataLayer push automatisch plant.

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>
//   );
// }

Eenvoudig testen: Globale Override

Om dit patroon snel te testen op je hele site of voor bestaande dataLayer.push()-implementaties zonder ze allemaal te refactoren, kun je de dataLayer.push()-functie globaal overschrijven.

Belangrijk: Plaats dit script hoog in de <head> van je HTML, direct na het laden van het GTM-containerscript. Dit zorgt ervoor dat je override zo snel mogelijk actief is.

<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>

Waarom dit de Interaction to Next Paint helpt

INP meet de latentie vanaf een gebruikersinteractie (bijv. klik, tik, toetsaanslag) tot het moment dat de browser de volgende visuele update schildert als reactie op die interactie. Als je resource-intensieve taken, zoals GTM-eventverwerking en het activeren van tags, synchroon uitvoert direct na een interactie, blokkeer je de main thread van de browser. Dit voorkomt het renderen van de visuele feedback die de gebruiker verwacht. Door niet-kritieke JavaScript-uitvoering zoals GTM-tracking uit te stellen tot nadat de browser de visuele updates heeft geschilderd, zorgt dit patroon ervoor dat gebruikers snelle visuele feedback ontvangen, wat de INP-score aanzienlijk verbetert.

Waarom niet gewoon een vaste vertraging, idle Callback of Scheduler gebruiken?

  • Vaste setTimeout(delay): Het gebruik van een hardcoded vertraging (bijv. setTimeout(..., 100)) is in wezen raden wanneer het renderen voltooid zal zijn. Het is niet adaptief; het kan te lang zijn (tracking onnodig vertragen) of te kort (paint nog steeds blokkeren).
  • requestIdleCallback: Deze API plant werk wanneer de browser idle is. Hoewel nuttig voor achtergrondtaken, garandeert het geen prompte uitvoering na de visuele update van een specifieke interactie. De callback kan veel later worden uitgevoerd of, tijdens drukke periodes, helemaal niet voordat de gebruiker wegnavigeert.
  • Generieke Schedulers (postTask etc.): Hoewel de postTask-scheduler van de browser prioritering biedt, is requestAnimationFrame specifiek gekoppeld aan de renderingcyclus. De awaitPaint-helper maakt hier gebruik van door requestAnimationFrame te gebruiken als signaal dat de browser zich voorbereidt om te painten, en voegt vervolgens een minimale vertraging toe om te activeren nadat die paint waarschijnlijk is voltooid.

Afwegingen

Elke optimalisatie heeft potentiële afwegingen.

  • Microvertraging in dataverzameling: Deze techniek introduceert een voorspelbare microvertraging (ruwweg 50-250ms, afhankelijk van de browserbelasting en de specifieke timeouts die worden gebruikt in awaitPaint) voordat eventgegevens Google Tag Manager bereiken.
  • Risico op dataverlies bij snel verlaten: Als een bezoeker een event triggert en vervolgens de pagina verlaat binnen dit microvertragingsvenster (voordat de uitgestelde dataLayer.push wordt uitgevoerd), worden die specifieke eventgegevens mogelijk niet verzonden. Voor kritieke events waarbij dit risico onaanvaardbaar is (bijv. direct voor een redirect of page unload), kunnen alternatieve trackingmechanismen zoals navigator.sendBeacon worden overwogen voor die specifieke events, hoewel dit buiten het bereik van de dataLayer.push-override valt.

Voor de meeste standaard analytics- en marketingtracking is deze lichte vertraging onbeduidend en een waardevolle afweging voor de significante verbetering in door de gebruiker waargenomen prestaties en INP-scores. Echter, voor ultra-lage-latentie scenario's (bijv. sommige vormen van real-time bidding-interacties die direct gekoppeld zijn aan GTM-events, of zeer gevoelige financiële dashboards waar milliseconde-precisie in eventlogging van het grootste belang is), is deze aanpak mogelijk niet geschikt.

Verder weegt de winst in INP-prestaties en user experience doorgaans ruimschoots op tegen de minimale latentie bij dataverzameling.Verder weegt de winst in INP-prestaties en user experience doorgaans ruimschoots op tegen de minimale latentie bij dataverzameling.

17 years of fixing PageSpeed.

I have optimized platforms for some of the largest publishers and e-commerce sites in Europe. I provide the strategy, the code, and the RUM verification. Usually in 1 to 2 sprints.

View Services
dataLayer Events plannen om de INP te optimaliserenCore Web Vitals dataLayer Events plannen om de INP te optimaliseren