DataLayer events plannen om de INP te optimaliseren

Hoe het uitstellen van GTM-events tot na de browser paint je INP-scores verbetert

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-03-05

TL;DR: INP verbeteren door dataLayer.push() uit te stellen

Wanneer dataLayer.push() synchroon vuurt binnen een event handler, verwerkt GTM tags voordat de browser visuele feedback kan painten. Dit blokkeert de main thread en verhoogt je Interaction to Next Paint (INP). De oplossing: plan dataLayer.push() om te draaien nadat de browser het volgende frame heeft gepaint met behulp van requestAnimationFrame. Dit patroon haalt op de meeste sites 20 tot 100 ms van de INP af, wat vaak een onvoldoende score omzet in een voldoende. Dataverzameling loopt 50 tot 250 ms vertraging op, wat onbeduidend is voor analytics en marketing tracking.

Hoe dataLayer.push() je INP schaadt

Voor een van onze klanten maten we een INP-reductie van 100 ms in Interaction to Next Paint (INP) door het moment waarop dataLayer.push() uitvoert na een gebruikersinteractie opnieuw in te plannen. De oplossing is een JavaScript drop-in vervanging die rendering prioriteit geeft boven tracking.

Laatst beoordeeld door Arjen Karel in maart 2026

Het probleem met synchrone dataLayer.push()

Als je met Google Tag Manager werkt, ken je dataLayer.push(). Het is de standaardmethode om events naar de Data Layer te sturen en het is diep geïntegreerd in de meeste sitefunctionaliteiten. Het probleem is dat niemand zich afvraagt wannéér het precies wordt uitgevoerd.

Wanneer dataLayer.push() direct binnen een event handler wordt aangeroepen (een klik op een knop, een formulierverzending, een menu-toggle), draait het synchroon. GTM-tags die zijn geconfigureerd om op dat event te vuren, worden ook onmiddellijk uitgevoerd, wat de main thread blokkeert voordat de browser de lay-out kan updaten. De bezoeker klikt op een knop en ziet niets gebeuren terwijl GTM tracking scripts op de achtergrond verwerkt. Die vertraging is je presentation delay en is de grootste oorzaak van een slechte INP.

De onderstaande performance trace, van een grote nieuwswebsite, toont GTM-activiteit na een gebruikersinteractie. De GTM-taken namen ongeveer 90 ms in beslag om uit te voeren, wat de totale INP naar 263 ms duwde. Die interactie zakt voor de Core Web Vitals.

De oplossing: eerst painten, dan pushen

De oplossing sluit aan bij de INP-optimalisatierichtlijnen van web.dev: yield to the main thread zodat de browser kan painten voordat niet-kritieke code wordt gedraaid. In plaats van tracking synchroon uit te voeren binnen de event handler:

  1. Voer de code voor de visuele update onmiddellijk uit.
  2. Laat de browser painten.
  3. Push daarna naar de dataLayer.

Hier is dezelfde interactie na het toepassen van dit patroon. De enige verandering is dat dataLayer.push() nu draait nadat de browser het volgende frame heeft gepaint.

De interactie die voorheen faalde, slaagt nu ruimschoots. Alle data bereikt nog steeds de dataLayer. Het verschil is dat GTM-scripts worden uitgevoerd nadat de browser heeft gereageerd op de actie van de gebruiker, niet daarvoor.

De awaitPaint helper

Deze helper gebruikt requestAnimationFrame om een callback in te plannen nadat de browser heeft gepaint. De geneste setTimeout zorgt ervoor dat de functie draait nadat de paint is voltooid, en niet vlak ervoor. Dit is het aanbevolen patroon van web.dev voor het uitstellen van niet-kritiek werk na een interactie.

async function awaitPaint(fn) {
    await new Promise((resolve) => {
        // Fallback: zorgt ervoor dat we niet blijven hangen als RAF nooit vuurt (bijv. achtergrondtabbladen)
        setTimeout(resolve, 200);

        // Inplannen na de volgende paint
        requestAnimationFrame(() => {
            setTimeout(resolve, 50);
        });
    });

    if (typeof fn === 'function') {
        fn();
    }
}

De helper gebruiken

Wikkel elke dataLayer.push() call in awaitPaint om deze uit te stellen tot na de paint:

function pushToDataLayer(event, data) {
    window.dataLayer = window.dataLayer || [];

    awaitPaint(() => {
        window.dataLayer.push({
            event,
            ...data,
            timestamp: new Date().toISOString()
        });
    });
}

// Gebruik
document.querySelector('.buy-button').addEventListener('click', () => {
    // Visuele feedback gebeurt onmiddellijk
    showLoadingSpinner();

    // Tracking vuurt na de paint
    pushToDataLayer('purchase_click', { productId: '123' });
});

Globale override voor snel testen

Om dit patroon op je hele site te testen zonder elke dataLayer.push() call te refactoren, kun je de push-functie globaal overschrijven. Plaats dit script in de <head> direct na de GTM-container snippet.

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

Dit overschrijft elke dataLayer.push() op de pagina. Alle bestaande GTM event triggers, hardcoded tracking calls en plugin-integraties profiteren hier automatisch van zonder enige codewijzigingen.

Waarom dit de INP verbetert

INP meet de tijd vanaf een gebruikersinteractie totdat de browser de visuele reactie paint. Als je GTM-eventverwerking synchroon uitvoert binnen een event handler, blokkeer je de main thread en verhinder je rendering. De Web Almanac van 2024 ontdekte dat presentation delay mediaan de grootste bijdrager is aan een slechte INP, en tracking scripts behoren tot de belangrijkste oorzaken.

De cijfers bevestigen dit. De Web Almanac van 2025 laat zien dat 77% van de mobiele pagina's over het algemeen slaagt voor de INP, maar slechts 63% van de top 1.000 meest bezochte sites doet dit. Die top sites laden meer third-party scripts. Bij Subito (de grootste marktplaats voor zoekertjes in Italië), zorgde het uitschakelen van één enkel TikTok tracking script geladen via GTM ervoor dat de INP daalde van 208 ms naar ongeveer 170 ms. Eén script, 38 ms bespaard.

Door dataLayer.push() uit te stellen tot na de paint, haal je alle GTM-verwerking uit het kritieke pad van de interactie. De gebruiker krijgt onmiddellijk visuele feedback. GTM vuurt nog steeds, alleen 50 tot 250 ms later.

Waarom geen vaste vertraging, Idle Callback of scheduler.yield()?

  • Vaste setTimeout(delay): Een hardcoded vertraging (bijv. setTimeout(..., 100)) is gokken wanneer de rendering voltooid zal zijn. Te lang en je stelt tracking onnodig uit. Te kort en je blokkeert nog steeds de paint.
  • requestIdleCallback: Plant werk in wanneer de browser idle is, maar garandeert geen vlotte uitvoering direct na een specifieke interactie. De callback draait mogelijk veel later, of helemaal niet voordat de gebruiker wegnavigeert.
  • scheduler.yield(): De moderne API voor yielding naar de main thread. Het behoudt taakprioriteit (in tegenstelling tot setTimeout die je voortzetting naar achteren in de wachtrij stuurt). scheduler.yield() garandeert echter niet dat er een paint heeft plaatsgevonden voordat je code draait. Voor deze specifieke use case is requestAnimationFrame de betere tool, omdat het gekoppeld is aan de rendering lifecycle. Let op dat scheduler.yield() nog niet wordt ondersteund in Safari.

De combinatie van requestAnimationFrame + setTimeout is specifiek ontworpen voor dit scenario. requestAnimationFrame vuurt net voordat de browser paint. De geneste setTimeout creëert een nieuwe taak die draait nadat de paint voltooid is. Samen garanderen ze dat je uitgestelde code uitvoert na de visuele update, wat exact is wat INP meet.

Afwegingen

  • Micro-vertraging in dataverzameling: Events bereiken GTM 50 tot 250 ms later dan met een synchrone push. Voor analytics en marketing tracking is dit onbeduidend.
  • Dataverlies bij een snelle exit: Als een bezoeker een event triggert en de pagina verlaat binnen de vertragingsperiode, vuurt dat event mogelijk niet. Gebruik voor kritieke events vóór een redirect of het sluiten van een pagina navigator.sendBeacon() in plaats van dataLayer.push(). Zie de argumenten voor het beperken van analytics scripts voor meer informatie over de Beacon API.

Voor de meeste tracking weegt de INP-winst ruimschoots op tegen de minimale vertraging. Als je op de milliseconde nauwkeurige event logging nodig hebt (real-time bidding, financiële dashboards), is deze aanpak niet geschikt. Voor al het andere is het een duidelijke winst.

Nadat je de override hebt toegepast, verifieer je de verbetering met Real User Monitoring. Labtests zullen het verschil aantonen, maar veldgegevens van echte gebruikers op echte netwerken is wat telt voor je Core Web Vitals-scores.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

Performance degrades unless you guard it.

I do not just fix the metrics. I set up the monitoring, the budgets, and the processes so your team keeps them green after I leave.

Start the Engagement
DataLayer events plannen om de INP te optimaliserenCore Web Vitals DataLayer events plannen om de INP te optimaliseren