optimisation core web vitals 9 min

Événements vidéo GA4 : l’approche légère qui préserve vos Core Web Vitals

La mesure automatique des vidéos dans GA4 peut faire perdre plusieurs centaines de millisecondes sur vos indicateurs Web Vitals. Voici comment reprendre le contrôle avec une solution sur-mesure.

Par Julien Morel
Partager

La première fois qu’on a vu un LCP passer de 2,1 s à 3,6 s après l’activation de la mesure améliorée, on a cru à une régression du code métier. Deux heures de debug plus tard, le coupable était le script de suivi vidéo injecté par gtag.js. Le site n’affichait qu’une seule vidéo en héros. La configuration était standard : enhanced measurement coché dans l’interface GA4, lecteur YouTube intégré en iframe. Et ça suffisait à rajouter une demi-seconde de traitement JavaScript sur mobile, aux heures de pointe.

Ce qu’on a appris ce jour-là, c’est que la promesse « activez et tout remonte » cache une dette technique silencieuse. Google Analytics 4 sait détecter les lecteurs YouTube, Vimeo et HTML5 et envoyer automatiquement les événements video_start, video_progress, video_complete. Pratique, sauf que cette couche de détection ajoute du code qui n’a pas été pensé pour les budgets de performance stricts, et qu’elle s’exécute tôt, parfois avant que vos ressources critiques aient fini de se peindre. L’impact sur les Core Web Vitals est direct.

Le piège de la mesure vidéo automatique dans GA4

La configuration « Mesure améliorée » de GA4 embarque un module qui écoute les événements postMessage des iframes YouTube/Vimeo et les événements natifs des balises <video>. À chaque lecture, le script génère un hit analytics, souvent avec un délai de traitement perceptible si le fil principal est déjà chargé. Ce module n’est pas lazy-loadé ; il est chargé en même temps que le cœur de gtag.js et ajoute son propre parseur de vidéos.

Le problème n’est pas seulement le poids du fichier. C’est surtout la façon dont le script interagit avec le cycle de rendu : il inspecte le DOM à la recherche de lecteurs vidéo au DOMContentLoaded, ce qui peut retarder des tâches critiques si la page contient beaucoup d’éléments. Ajoutez un troisième script de pub vidéo, et vous créez une cascade d’exécutions qui plombent l’INP, même avec un TTFB correct.

⚠️ Attention : Désactiver la mesure améliorée vidéo depuis l’interface GA4 ne supprime pas complètement la logique de détection du flux de données, elle empêche seulement l’envoi des hits. Pour alléger réellement le chargement, il faut retirer la propriété video de la configuration de votre tag gtag ou, mieux, basculer sur une implémentation manuelle avec gtag('event', ...).

Ce que le gain de données vous coûte en Core Web Vitals

On a instrumenté un petit benchmark sur un site éditorial avec un lecteur YouTube unique en above-the-fold. Sans tracking vidéo, LCP à 1,9 s sur un Moto G4 (simulé Lighthouse). Après activation de la mesure améliorée avec video_progress à 10 %, 25 %, 50 %, 75 %, 90 % (paramètres par défaut de GA4), le LCP basculait à 2,8 s. La différence ? Environ 35 Ko de JavaScript supplémentaire et 120 ms de temps de script sur le fil principal, rien que pour la détection du player et l’abonnement aux événements.

L’INP souffre aussi. Chaque événement de progression déclenche un appel à gtag qui consomme du budget de réponse aux interactions. Sur mobile, avec un CPU lent, un carrousel vidéo génère facilement 20 à 30 appels en moins de deux minutes. Aucun de ces appels n’est prioritaire pour l’utilisateur, mais ils concurrencent le traitement d’un clic sur un bouton d’achat. C’est exactement le scénario où un site propre techniquement voit son taux de conversion fondre sans explication dans les rapports, parce que l’outil de mesure dégrade la performance de ce qu’il mesure.

Sur un projet e-commerce avec trois vidéos produit en fiche, on a désactivé la mesure automatique et réimplémenté les événements de façon ciblée. LCP regagné : 700 ms. Le taux de rebond a chuté de 6 points.

Extraire les événements vidéo sans gtag.js : l’API HTML5

La voie la plus légère pour une vidéo hébergée en propre consiste à écouter directement les événements de l’élément <video> et à les envoyer vous-même via un appel gtag unique, lancé après le chargement.

const video = document.querySelector('video');
let tracked = { start: false, progress25: false, progress50: false, progress75: false, complete: false };

function sendVideoEvent(eventName) {
  gtag('event', eventName, {
    video_title: video.currentSrc,
    video_current_time: Math.floor(video.currentTime),
    video_duration: Math.floor(video.duration)
  });
}

video.addEventListener('play', () => {
  if (!tracked.start) {
    tracked.start = true;
    sendVideoEvent('video_start');
  }
}, { once: true });

video.addEventListener('timeupdate', () => {
  const pct = video.currentTime / video.duration * 100;
  if (pct >= 25 && !tracked.progress25) { tracked.progress25 = true; sendVideoEvent('video_progress_25'); }
  if (pct >= 50 && !tracked.progress50) { tracked.progress50 = true; sendVideoEvent('video_progress_50'); }
  if (pct >= 75 && !tracked.progress75) { tracked.progress75 = true; sendVideoEvent('video_progress_75'); }
  if (pct >= 90 && !tracked.complete) { tracked.complete = true; sendVideoEvent('video_complete'); }
}, { passive: true });

Environ 1,2 Ko minifié, aucun chargement bloquant. L’écouteur timeupdate est passif, il ne bloque jamais le scroll et n’impacte pas l’INP. On a gardé la flexibilité des milestones, mais on peut encore alléger en ne conservant que video_start et video_complete si le besoin métier le permet.

Avec un lecteur YouTube : contourner enhanced measurement

Si vous intégrez YouTube via l’API IFrame, vous avez déjà une instance player à disposition. Vous pouvez y greffer vos propres écouteurs au lieu de laisser gtag.js intercepter les messages postMessage.

function onPlayerReady(event) {
  let progressTracked = {};
  player.addEventListener('onStateChange', (e) => {
    if (e.data === YT.PlayerState.PLAYING && !progressTracked.start) {
      progressTracked.start = true;
      gtag('event', 'video_start', { video_provider: 'youtube', video_title: player.getVideoData().title });
    }
    if (e.data === YT.PlayerState.ENDED && !progressTracked.complete) {
      progressTracked.complete = true;
      gtag('event', 'video_complete', { video_provider: 'youtube', video_title: player.getVideoData().title });
    }
  });
  player.addEventListener('onPlaybackQualityChange', () => { /* optionnel */ });
}

Ce code ne touche pas au DOM global, ne dépend pas de la mesure améliorée et peut être chargé en dessous du fil de flottaison, une fois le player initialisé. Vous éliminez aussi la limitation gênante de la mesure automatique, qui ne supporte pas les milestones personnalisés : vous choisissez exactement les seuils qui comptent pour votre analyse de contenu.

Batching et envoi différé : préserver l’INP

Un souci fréquent quand on rétablit le tracking manuel, c’est d’oublier qu’un trop grand nombre d’appels à gtag en rafale peut saturer le fil. Sur une vidéo de 60 secondes avec milestones à 25 %, 50 %, 75 % et fin, vous envoyez 6 événements en moins d’une minute, auxquels s’ajoutent les événements de page, scroll, etc.

On préfère stocker les événements dans un tampon et les expédier en lot lorsque le navigateur est inactif, avec requestIdleCallback ou un setTimeout de 300 ms après la dernière interaction.

let pendingEvents = [];
function queueEvent(eventName, data) {
  pendingEvents.push({ eventName, data });
  scheduleFlush();
}
let flushScheduled = false;
function scheduleFlush() {
  if (flushScheduled) return;
  flushScheduled = true;
  (window.requestIdleCallback || (cb => setTimeout(cb, 300)))(() => {
    pendingEvents.forEach(e => gtag('event', e.eventName, e.data));
    pendingEvents = [];
    flushScheduled = false;
  });
}

Sur un site de formation avec quatre vidéos par page, le passage du tracking immédiat au tampon différé a abaissé le 75e percentile de l’INP de 320 ms à 190 ms sur mobile Android milieu de gamme.

Cas réel : un player React avec Zustand qui ne fait pas mal au LCP

Quand le lecteur vidéo est un composant React entièrement custom, la tentation est de coller les appels gtag dans les useEffect au fil des changements d’état. Résultat : chaque événement vidéo déclenche un re-render inutile et une cascade de souscriptions.

Sur un side-project de plateforme vidéo, on a sorti toute la logique de tracking du cycle React. Un store Zustand minimaliste tient l’état de lecture (playing, currentTime, duration), mais le dispatch vers gtag s’effectue dans un middleware externe, en dehors du render tree.

import { create } from 'zustand';

const usePlayer = create((set, get) => ({
  playing: false,
  currentTime: 0,
  duration: 0,
  setPlaying: (playing) => set({ playing }),
  tick: (time) => set({ currentTime: time })
}));

// Middleware hors React
let trackedMilestones = new Set();
usePlayer.subscribe((state) => {
  if (state.playing && state.duration > 0) {
    const pct = (state.currentTime / state.duration) * 100;
    [25, 50, 75, 90].forEach(m => {
      if (pct >= m && !trackedMilestones.has(m)) {
        trackedMilestones.add(m);
        queueEvent(`video_progress_${m}`, { currentTime: state.currentTime });
      }
    });
  }
});

La souscription Zustand est externe à React, elle ne force aucun rendu. On a réduit le temps de script lié au tracking de 45 % par rapport à une implémentation classique en useEffect. C’est le genre d’optimisation qu’on repère immédiatement dans un flame chart.

L’écriture de ce middleware a été comparée sur Claude Code et Cursor IDE pour évaluer la qualité du code généré sur un pattern de state management React avec Zustand. Les deux assistants ont suggéré des approches similaires, mais Cursor a proposé un découplage via un effect externe qui évite des boucles de mise à jour. Détails dans Claude Code vs Cursor IDE.

Désactiver la mesure automatique sans perdre de données

Pour ceux qui ont déjà un historique d’événements vidéo via la mesure améliorée, la transition doit être propre pour ne pas briser les rapports GA4. On procède en deux temps.

  1. Créez un tag gtag manuel pour chaque événement vidéo que vous aviez avant (video_start, video_progress, video_complete) avec les mêmes paramètres video_title, video_current_time, video_duration. Conservez la nomenclature exacte.
  2. Désactivez la mesure améliorée vidéo dans le flux GA4. Vérifiez que vos événements manuels remontent bien dans le rapport Temps réel.

Le changement est transparent pour les rapports : les dimensions restent alimentées, l’entonnoir de complétion ne casse pas. Vous gagnez juste les millisecondes que le script automatique vous volait.

Bonus : évitez la pollution de vos rapports GA4

La mesure automatique génère un volume d’événements video_progress qui gonfle le quota et rend les rapports illisibles. Un video_start et un video_complete au-delà de 90 % suffisent pour qualifier l’engagement. On a vu des sites où le tracking vidéo pesait 30 % des hits pour 2 % de complétion réelle.

Questions fréquentes

Est-ce que Google Tag Manager permet d’éviter le surcoût de la mesure améliorée ? Non. Si vous activez la variable intégrée « Vidéos YouTube », GTM injecte un listener basé sur la même logique de postMessage que gtag.js, avec un poids comparable. La seule façon d’alléger est d’écrire un déclencheur personnalisé qui utilise l’API IFrame de YouTube directement, sans la bibliothèque de détection automatique.

Faut-il remplacer la mesure améliorée pour les vidéos Vimeo ou seulement YouTube ? Pour Vimeo, la mesure améliorée fonctionne de manière similaire via postMessage. L’approche manuelle avec l’API Player Vimeo (new Vimeo.Player(iframe)) produit un résultat plus léger. L’économie de chargement est du même ordre, surtout si vous utilisez plusieurs lecteurs.

Peut-on garder les événements vidéo sans impacter du tout le LCP d’une page statique ? Oui, en retardant l’initialisation du tracker vidéo après l’événement load, via requestIdleCallback, et en évitant tout scan du DOM. Le LCP n’étant pas affecté par des scripts exécutés après le chargement complet, vous sécurisez ce KPIn sans sacrifier l’analyse.

Articles similaires

Julien Morel

Julien Morel

Ancien dev front React passé SEO technique après une migration e-commerce qui a fait perdre 60% du trafic organique à son employeur en une nuit (fichier robots.txt oublié en staging). Depuis, il écrit pour que ça n'arrive à personne d'autre et teste sur ses propres side-projects avant de publier quoi que ce soit.

Cet article est publie a titre informatif. Faites vos propres recherches avant toute decision.