Planifier les événements dataLayer pour optimiser l'INP
Comment différer les événements GTM après le paint du navigateur améliore les scores INP
Partager
TL;DR : améliorer l'INP en différant dataLayer.push()
Quand dataLayer.push() s'exécute de manière synchrone dans un event handler, GTM traite les balises avant que le navigateur ne puisse afficher le retour visuel. Cela bloque le main thread et fait grimper l'Interaction to Next Paint (INP). Le fix : planifier l'exécution de dataLayer.push() après le paint de la frame suivante via requestAnimationFrame. Ce pattern permet de gagner 20 à 100 ms d'INP sur la plupart des sites, transformant souvent un score d'échec en score validé. La collecte des données est différée de 50 à 250 ms, ce qui est sans conséquence pour le tracking analytics et marketing.
Comment dataLayer.push() dégrade l'INP
Pour l'un de nos clients, nous avons mesuré un gain de 100 ms sur l'Interaction to Next Paint (INP) en reprogrammant l'exécution de dataLayer.push() après une interaction utilisateur. Le fix est un remplacement direct en JavaScript qui donne la priorité au rendu sur le tracking.
Dernière révision par Arjen Karel en mars 2026
Le problème du dataLayer.push() synchrone
Si vous travaillez avec Google Tag Manager, vous connaissez dataLayer.push(). C'est la méthode standard pour envoyer des événements au dataLayer, et elle est profondément intégrée au fonctionnement de la plupart des sites. Le problème, c'est que personne ne se demande quand il s'exécute.
Quand dataLayer.push() est appelé directement dans un event handler (clic sur un bouton, soumission de formulaire, ouverture de menu), il s'exécute de manière synchrone. Les balises GTM configurées pour se déclencher sur cet événement s'exécutent aussi immédiatement, bloquant le main thread avant que le navigateur ne mette à jour l'affichage. Le visiteur clique sur un bouton et ne voit rien se passer pendant que GTM traite les scripts de tracking en arrière-plan. Ce retard constitue le presentation delay, et c'est le principal responsable d'un mauvais INP.
La trace de performance ci-dessous, issue d'un grand site d'actualités, montre l'activité de GTM après une interaction utilisateur. Les tâches GTM ont nécessité environ 90 ms d'exécution, portant l'INP global à 263 ms. Cette interaction échoue aux Core Web Vitals.

Le fix : d'abord le paint, ensuite le push
Cette solution suit les recommandations d'optimisation de l'INP de web.dev : yield to the main thread pour que le navigateur puisse effectuer le paint avant d'exécuter du code non critique. Au lieu d'exécuter le tracking de manière synchrone dans l'event handler :
- Exécutez immédiatement le code de mise à jour visuelle.
- Laissez le navigateur effectuer le paint.
- Poussez ensuite les données vers le dataLayer.
Voici la même interaction après application de ce pattern. Seul changement : dataLayer.push() s'exécute désormais après que le navigateur a affiché la frame suivante.

L'interaction qui échouait auparavant passe désormais sans problème. Toutes les données parviennent toujours au dataLayer. La différence, c'est que les scripts GTM s'exécutent après que le navigateur a répondu à l'action de l'utilisateur, et non avant.
Le helper awaitPaint
Ce helper utilise requestAnimationFrame pour planifier un callback après le paint du navigateur. Le setTimeout imbriqué garantit que la fonction s'exécute une fois le paint terminé, et pas seulement juste avant. C'est le pattern recommandé par web.dev pour différer les tâches non critiques après une interaction.
async function awaitPaint(fn) {
await new Promise((resolve) => {
// Fallback : évite tout blocage si le RAF ne se déclenche jamais (ex. onglets en arrière-plan)
setTimeout(resolve, 200);
// Planifie après le prochain paint
requestAnimationFrame(() => {
setTimeout(resolve, 50);
});
});
if (typeof fn === 'function') {
fn();
}
} Utiliser le helper
Encapsulez chaque appel à dataLayer.push() dans awaitPaint pour le différer après le paint :
function pushToDataLayer(event, data) {
window.dataLayer = window.dataLayer || [];
awaitPaint(() => {
window.dataLayer.push({
event,
...data,
timestamp: new Date().toISOString()
});
});
}
// Utilisation
document.querySelector('.buy-button').addEventListener('click', () => {
// Le retour visuel s'affiche immédiatement
showLoadingSpinner();
// Le tracking se déclenche après le paint
pushToDataLayer('purchase_click', { productId: '123' });
}); Surcharge globale pour tester rapidement
Pour tester ce pattern sur l'ensemble de votre site sans refactoriser chaque appel à dataLayer.push(), vous pouvez surcharger globalement la fonction push. Placez ce script dans le <head>, immédiatement après le snippet du conteneur GTM.
<script type="module">
window.dataLayer = window.dataLayer || [];
async function awaitPaint(fn) {
await new Promise((resolve) => {
// Fallback : évite tout blocage si le RAF ne se déclenche jamais (ex. onglets en arrière-plan)
setTimeout(resolve, 200);
// Planifie après le prochain paint
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> Cela surcharge chaque dataLayer.push() de la page. Tous les déclencheurs d'événements GTM existants, les appels de tracking codés en dur et les intégrations de plugins en profitent automatiquement, sans aucun changement de code.
Pourquoi cela améliore l'INP
L'INP mesure le temps écoulé entre l'interaction d'un utilisateur et l'affichage de la réponse visuelle par le navigateur. Exécuter le traitement des événements GTM de manière synchrone dans un event handler bloque le main thread et empêche le rendu. Le Web Almanac 2024 a montré que le presentation delay est le principal facteur d'un mauvais INP au niveau de la médiane, et les scripts de tracking en sont l'une des causes majeures.
Les chiffres le confirment. Le Web Almanac 2025 montre que 77 % des pages mobiles valident l'INP dans l'ensemble, mais seulement 63 % des 1 000 sites les plus visités y parviennent. Ces sites majeurs chargent davantage de scripts tiers. Chez Subito (le plus grand site de petites annonces en Italie), la désactivation d'un seul script de tracking TikTok chargé via GTM a fait baisser l'INP de 208 ms à environ 170 ms. Un seul script, 38 ms de gagnées.
En différant dataLayer.push() après le paint, vous sortez l'ensemble du traitement GTM du chemin critique de l'interaction. L'utilisateur obtient un retour visuel immédiat. GTM se déclenche toujours, simplement 50 à 250 ms plus tard.
Pourquoi pas un délai fixe, Idle Callback ou scheduler.yield() ?
setTimeout(delay)fixe : un délai codé en dur (ex. :setTimeout(..., 100)) revient à deviner la fin du rendu. Trop long, vous retardez le tracking inutilement. Trop court, vous bloquez toujours le paint.requestIdleCallback: planifie le travail lorsque le navigateur est inactif, mais ne garantit pas une exécution rapide après une interaction donnée. Le callback peut s'exécuter bien plus tard, voire pas du tout si l'utilisateur quitte la page.scheduler.yield(): l'API moderne de yielding vers le main thread. Elle préserve la priorité des tâches (contrairement àsetTimeoutqui relègue la suite de l'exécution en fin de file). Cependant,scheduler.yield()ne garantit pas qu'un paint a eu lieu avant l'exécution du code. Pour ce cas d'usage précis,requestAnimationFramereste le meilleur outil car il est directement lié au cycle de rendu. Notez quescheduler.yield()n'est pas encore pris en charge par Safari.
La combinaison requestAnimationFrame + setTimeout est spécialement pensée pour ce scénario. requestAnimationFrame se déclenche juste avant le paint par le navigateur. Le setTimeout imbriqué crée une nouvelle tâche qui s'exécute une fois ce paint terminé. Ensemble, ils garantissent que votre code différé s'exécute après la mise à jour visuelle — exactement ce que mesure l'INP.
Compromis
- Micro-délai dans la collecte des données : les événements parviennent à GTM 50 à 250 ms plus tard qu'avec un push synchrone. Pour le tracking analytics et marketing, c'est sans conséquence.
- Perte de données en cas de sortie rapide : si un visiteur déclenche un événement puis quitte la page pendant cet intervalle, l'événement risque de ne pas partir. Pour les événements critiques précédant une redirection ou le déchargement de la page, utilisez
navigator.sendBeacon()plutôt quedataLayer.push(). Consultez l'intérêt de limiter les scripts analytics pour en savoir plus sur l'API Beacon.
Pour la plupart des trackings, le gain sur l'INP surpasse largement ce délai minime. Si vous avez besoin d'une précision à la milliseconde pour la journalisation des événements (enchères en temps réel, tableaux de bord financiers), cette approche ne convient pas. Pour tout le reste, le bénéfice est évident.
Après avoir appliqué la surcharge, vérifiez les gains avec du Real User Monitoring. Les lab data montreront la différence, mais ce sont les field data issues d'utilisateurs réels sur des réseaux réels qui comptent pour vos scores Core Web Vitals.

Besoin d'un site plus rapide ?
Je suis Arjen et je répare des sites lents depuis 17 ans. Si vous rencontrez des problèmes que vous ne pouvez pas résoudre vous-même, envoyez-moi votre URL et parlons-en.
Envoyez-moi votre URL
Pourquoi notre fix ne fonctionne pas ?
Parfois le CDN sert encore l'ancien HTML, ou le fix a été appliqué sur le template desktop alors que le mobile en utilise un autre. Envoyez-moi votre URL et le problème, et je vous réponds avec quelques créneaux. J'analyse d'abord vos field data, puis nous passons une heure sur votre site, et sous 24 heures vous avez mes notes par écrit. 350 € HT.
Envoyez-moi votre URLPas de développeur pour réaliser le fix ? Je fais le travail moi-même, sur une copie de votre site.