INP : diagnostiquer et corriger une Interaction to Next Paint lente

Un e-commerce mode de 12 000 pages produit. CrUX report : INP à 547ms sur mobile. Depuis mars 2024 et le remplacement officiel de FID par INP dans les Core Web Vitals, cette métrique pèse directement dans l'évaluation page experience de Google. Le site perdait des positions sur des requêtes transactionnelles à forte concurrence — pas à cause du contenu, mais parce que chaque clic sur un filtre de taille ou un ajout au panier gelait l'interface pendant plus d'une demi-seconde.

INP n'est pas FID. FID mesurait le délai avant le premier event handler. INP mesure la latence complète de l'interaction la plus lente sur toute la durée de la session : du clic (ou tap, ou keypress) jusqu'au prochain frame peint à l'écran. C'est une métrique radicalement plus exigeante.

Anatomie d'une interaction : les trois phases d'INP

INP décompose chaque interaction en trois phases consécutives. Comprendre cette décomposition est le prérequis pour savoir où agir.

Input delay

Le temps entre le moment où l'utilisateur interagit et le moment où le premier event handler commence à s'exécuter. Ce délai est souvent causé par du JavaScript déjà en cours d'exécution sur le main thread — un script tiers qui parse un payload, un hydration en cours, un timer qui bloque.

Processing time

La durée d'exécution de tous les event handlers associés à l'interaction. Si un clic déclenche un click handler, un pointerup handler et un mouseup handler, le processing time couvre les trois. C'est ici que votre code applicatif entre en jeu.

Presentation delay

Le temps entre la fin du dernier event handler et le moment où le navigateur peint le prochain frame. Le navigateur doit recalculer les styles, exécuter le layout, le paint, le compositing. Si vos handlers provoquent un reflow massif (modification du DOM qui affecte des centaines d'éléments), cette phase explose.

Le seuil Google : une interaction est "bonne" sous 200ms, "à améliorer" entre 200 et 500ms, "mauvaise" au-delà de 500ms. INP retient la pire interaction de la session (avec un ajustement statistique pour les sessions longues : approximativement le 98e percentile).

La documentation officielle de Google détaille ce mécanisme : Optimize INP.

Mesurer INP : les bons outils, les bons réflexes

Données de terrain (RUM)

INP est une métrique de terrain par nature. Elle dépend du device de l'utilisateur, de ce qu'il fait sur la page, de quand il interagit. Vous ne pouvez pas la reproduire de manière fiable en lab.

Chrome User Experience Report (CrUX) via Search Console ou PageSpeed Insights donne le P75 d'INP sur 28 jours glissants. C'est la donnée que Google utilise pour le ranking. Si votre Search Console affiche un INP "mauvais" dans le rapport Core Web Vitals, c'est cette donnée CrUX.

Pour un diagnostic plus fin, la bibliothèque web-vitals de Google permet d'instrumenter votre RUM :

import { onINP } from 'web-vitals/attribution';

onINP((metric) => {
  const entry = metric.attribution;
  
  const report = {
    value: metric.value,
    interactionTarget: entry.interactionTarget, // sélecteur CSS de l'élément cliqué
    interactionType: entry.interactionType,      // "pointer", "keyboard"
    inputDelay: entry.inputDelay,
    processingDuration: entry.processingDuration,
    presentationDelay: entry.presentationDelay,
    longAnimationFrameEntries: entry.longAnimationFrameEntries,
  };
  
  // Envoi vers votre endpoint analytics
  navigator.sendBeacon('/api/rum', JSON.stringify(report));
}, { reportAllChanges: true });

Le build attribution est essentiel. Sans lui, vous obtenez la valeur brute d'INP sans savoir quelle interaction pose problème ni quelle phase domine. La décomposition inputDelay / processingDuration / presentationDelay oriente directement votre investigation.

Données lab et debugging

Chrome DevTools > Performance panel : enregistrez une trace, interagissez avec la page, puis inspectez les "Interactions" track. Chaque interaction affiche sa durée totale et les long tasks associées. La vue "Main" thread montre exactement quel script bloque.

Long Animation Frames (LoAF) API : successeur des Long Tasks, cette API expose les frames qui dépassent 50ms avec le détail des scripts responsables. Chrome DevTools l'intègre nativement depuis Chrome 123.

Un point subtil : les mesures lab sous-estiment systématiquement INP. Votre machine de développement avec un i9 et 32Go de RAM n'a rien à voir avec le smartphone milieu de gamme de vos utilisateurs. Pour tester en conditions réalistes, utilisez le throttling CPU 4x dans DevTools et testez sur des devices réels via Chrome DevTools remote debugging.

Les cinq causes d'un INP dégradé (avec code)

Cause 1 : long tasks JavaScript sur le main thread

Le cas le plus fréquent. Un script exécute une opération de plus de 50ms, bloquant le main thread. Si l'utilisateur interagit pendant ce temps, l'input delay explose.

Scénario concret : sur le site e-commerce mentionné plus haut, le composant de filtrage produit recalculait le rendu de 200 cartes produit de manière synchrone à chaque changement de filtre. Processing time : 380ms sur un Pixel 6.

La solution : découper le travail via scheduler.yield() (disponible nativement depuis Chrome 129, avec polyfill pour les autres navigateurs) :

async function renderFilteredProducts(products) {
  const container = document.getElementById('product-grid');
  const fragment = document.createDocumentFragment();
  
  const CHUNK_SIZE = 20;
  
  for (let i = 0; i < products.length; i += CHUNK_SIZE) {
    const chunk = products.slice(i, i + CHUNK_SIZE);
    
    chunk.forEach(product => {
      const card = createProductCard(product);
      fragment.appendChild(card);
    });
    
    // Rend la main au navigateur entre chaque batch
    if (i + CHUNK_SIZE < products.length) {
      await scheduler.yield();
    }
  }
  
  container.replaceChildren(fragment);
}

// Polyfill minimaliste pour les navigateurs sans scheduler.yield()
if (!globalThis.scheduler?.yield) {
  globalThis.scheduler = globalThis.scheduler || {};
  globalThis.scheduler.yield = () => {
    return new Promise(resolve => setTimeout(resolve, 0));
  };
}

Le scheduler.yield() est supérieur à setTimeout(fn, 0) parce qu'il conserve la priorité de la tâche en cours. Avec setTimeout, votre tâche est repoussée en fin de queue et des scripts tiers peuvent s'intercaler. Avec scheduler.yield(), la continuation reprend prioritairement.

Résultat sur le site e-commerce : processing time passé de 380ms à 45ms. INP global (P75 CrUX) : de 547ms à 168ms en 4 semaines.

Cause 2 : hydration bloquante sur les frameworks JavaScript

Si votre site tourne sur React, Vue ou Angular avec SSR, l'hydration est un moment critique. Pendant que le framework attache les event listeners et réconcilie le DOM, le main thread est saturé. Toute interaction utilisateur pendant cette fenêtre subit un input delay massif.

Sur un SPA React de 8 000 pages (marketplace B2B), l'hydration prenait 1.2 secondes sur mobile. Chaque clic sur le menu pendant les deux premières secondes avait un INP supérieur à 800ms.

Les solutions selon le framework :

  • React 18+ : React.lazy() + Suspense pour hydrater les composants interactifs au-dessus de la fold en priorité. React Server Components (RSC) réduisent le volume de JS envoyé au client.
  • Next.js : le choix du mode de rendering impacte directement l'INP. L'ISR avec des composants serveur limite le JS client. Les Server Actions évitent d'embarquer du code de mutation côté client.
  • Vue/Nuxt 3 : les composants <ClientOnly> permettent de différer l'hydration des composants non critiques.
  • Angular : le defer loading (@defer) avec triggers (on viewport, on interaction) permet une hydration incrémentale native.

Le pattern transversal : réduire la quantité de JavaScript qui doit s'exécuter avant que la page soit interactive. Chaque Ko de JS parsé et exécuté au chargement est un risque d'input delay sur les premières interactions.

Cause 3 : scripts tiers non contrôlés

Google Tag Manager, pixels publicitaires, widgets de chat, A/B testing... Ces scripts s'exécutent souvent en mode "fire and forget" sur le main thread. Un seul script tiers qui prend 200ms à s'exécuter peut ruiner votre INP si son exécution coïncide avec une interaction.

La stratégie : charger les scripts tiers non critiques avec requestIdleCallback ou via un Partytown (qui déplace leur exécution dans un Web Worker) :

<!-- Au lieu de charger directement -->
<!-- <script src="https://analytics.example.com/tracker.js"></script> -->

<!-- Différer au idle -->
<script>
  function loadThirdParty(src) {
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        const s = document.createElement('script');
        s.src = src;
        s.async = true;
        document.head.appendChild(s);
      }, { timeout: 3000 });
    } else {
      // Fallback : chargement après le load event
      window.addEventListener('load', () => {
        setTimeout(() => {
          const s = document.createElement('script');
          s.src = src;
          s.async = true;
          document.head.appendChild(s);
        }, 1000);
      });
    }
  }
  
  loadThirdParty('https://analytics.example.com/tracker.js');
  loadThirdParty('https://chat-widget.example.com/widget.js');
</script>

Attention au trade-off : différer les scripts d'analytics peut fausser vos données de trafic (sessions très courtes non trackées). Différer un script d'A/B testing peut causer un flash of original content. Évaluez script par script. Un audit via le Performance panel de DevTools avec la colonne "Third-party" activée vous montre l'impact exact de chaque tiers.

Cause 4 : layout thrashing dans les event handlers

Un handler qui lit puis écrit dans le DOM de manière alternée force le navigateur à recalculer le layout à chaque lecture. C'est le presentation delay qui explose.

// ❌ Layout thrashing
function updatePrices(items) {
  items.forEach(item => {
    const el = document.getElementById(`price-${item.id}`);
    const currentHeight = el.offsetHeight;  // Force layout read
    el.style.height = `${currentHeight + 10}px`;  // Force layout write
    // Au prochain tour : re-lecture = re-layout forcé
  });
}

// ✅ Batch reads, puis batch writes
function updatePrices(items) {
  // Phase lecture
  const measurements = items.map(item => {
    const el = document.getElementById(`price-${item.id}`);
    return { el, height: el.offsetHeight };
  });
  
  // Phase écriture (un seul recalcul de layout)
  measurements.forEach(({ el, height }) => {
    el.style.height = `${height + 10}px`;
  });
}

Si vous utilisez un framework réactif (React, Vue), le virtual DOM gère normalement ce batching. Mais attention aux useLayoutEffect en React ou aux $nextTick en Vue qui peuvent forcer des synchronous layouts.

Cause 5 : animations et transitions CSS coûteuses

Les animations qui modifient width, height, top, left, ou margin déclenchent layout + paint à chaque frame. Si une interaction lance une telle animation, le presentation delay grimpe.

Préférez systématiquement transform et opacity, qui sont composités par le GPU sans passer par layout/paint. Ajoutez will-change: transform sur les éléments concernés (mais pas globalement — c'est un hint, pas une solution miracle, et il consomme de la mémoire GPU).

Stratégie d'optimisation INP pour un site à grande échelle

Prenons un cas réel : un média en ligne, 25 000 articles, stack Next.js 14, INP P75 à 412ms d'après CrUX. Le trafic organique stagnait malgré un contenu éditorial fort. L'analyse des Core Web Vitals et leur impact sur le classement montrait que les pages article échouaient sur INP alors que LCP et CLS étaient corrects.

Phase 1 : identifier les interactions problématiques

Déploiement de web-vitals/attribution en production pendant 14 jours. Résultat : 73% des interactions lentes venaient de deux éléments — le menu hamburger mobile et le bouton "Voir plus de commentaires" en bas d'article.

Le menu hamburger déclenchait un re-render complet de la navigation (120 liens avec des sous-menus imbriqués). Le bouton commentaires fetchait et rendait 50 commentaires d'un coup.

Phase 2 : corrections ciblées

Menu : extraction dans un composant <ClientOnly> avec lazy loading. Le HTML du menu est rendu côté serveur (SEO préservé), mais l'interactivité n'est hydratée que quand l'utilisateur scroll vers le header ou clique dessus. Utilisation du pattern content-visibility: auto sur les sous-menus pour éviter le coût de rendu initial.

Commentaires : passage à un rendu incrémental — afficher 10 commentaires puis charger les suivants via IntersectionObserver. Processing time divisé par 5.

Phase 3 : nettoyage des scripts tiers

L'audit DevTools révélait 14 scripts tiers. Quatre d'entre eux exécutaient des long tasks de plus de 100ms. Le script d'A/B testing (780ms de processing) a été migré vers un edge worker Cloudflare. Deux pixels marketing ont été passés en chargement idle. Le widget de consentement cookies a été remplacé par une solution plus légère (de 250Ko à 18Ko de JS).

Résultat

Six semaines après le déploiement complet, le CrUX montrait un INP P75 à 156ms. Le rapport Core Web Vitals de Search Console passait au vert sur 100% des pages. Sur les trois mois suivants, le trafic organique sur les pages article a progressé de 8% — corrélation, pas causalité prouvée, mais cohérent avec le fait que ces pages concouraient sur des requêtes à forte compétition où le tie-breaker page experience fait la différence.

INP et le rendering côté serveur : un faux sentiment de sécurité

Le SSR ne résout pas automatiquement les problèmes d'interactivité. Il améliore LCP et le rendu initial visible par Googlebot, mais INP est une métrique purement client-side. Un site SSR qui envoie 800Ko de JavaScript pour l'hydration aura un INP catastrophique.

Le piège classique : migrer d'une SPA vers du SSR/SSG en pensant que les Core Web Vitals passeront au vert automatiquement. LCP et CLS s'améliorent, mais INP peut se dégrader si le bundle JS grossit (hydration plus lourde) ou si la stratégie de prerendering n'est pas accompagnée d'un travail sur le JS client.

L'objectif : minimiser le JavaScript qui s'exécute côté client. Chaque interaction doit pouvoir être traitée en moins de 200ms sur un device avec un processeur 4x plus lent que votre MacBook.

Monitoring continu : détecter les régressions INP avant qu'elles n'impactent CrUX

INP est une métrique glissante sur 28 jours dans CrUX. Une régression déployée aujourd'hui ne sera pleinement visible dans Search Console que dans 4 semaines. Quatre semaines pendant lesquelles vos pages accumulent des données "mauvaises" qui diluent lentement votre score.

C'est le même type de régression silencieuse qu'une balise meta robots noindex déployée par erreur ou un canonical mal configuré — sauf que celle-ci ne se voit pas dans un crawl technique classique.

La chaîne de détection idéale :

  1. RUM en production avec web-vitals/attribution, agrégation par template de page, alerting si le P75 dépasse 200ms sur un template donné.
  2. Synthetic monitoring : Lighthouse CI ou WebPageTest automatisé sur vos templates clés à chaque déploiement. Pas suffisant seul (données lab ≠ terrain), mais utile pour détecter une régression évidente avant le merge.
  3. Monitoring SEO technique : un outil comme Seogard qui surveille en continu les métriques de performance et détecte les régressions dès qu'elles apparaissent, sans attendre le prochain rapport CrUX.

Le point critique est la corrélation entre les changements de code et les variations d'INP. Taguez vos données RUM avec le hash de commit ou le numéro de déploiement. Quand INP se dégrade, vous devez pouvoir remonter au déploiement responsable en minutes, pas en jours.

Edge cases et trade-offs à connaître

INP sur les pages à faible trafic : CrUX ne remonte des données que pour les pages (ou groupes de pages) ayant suffisamment de sessions. Sur un site avec beaucoup de pages longue traîne à 10-20 visites par mois, vous n'aurez pas de données INP granulaires. Google utilise alors les données au niveau de l'origine (domaine entier). Une page à fort trafic avec un mauvais INP peut donc pénaliser l'ensemble du site.

INP et les interactions non visuelles : un clic qui déclenche un fetch() sans feedback visuel immédiat sera mesuré par INP jusqu'au prochain paint. Si votre handler fait un appel réseau de 300ms avant d'afficher un résultat, INP sera de 300ms+. La solution : afficher un état de chargement (spinner, skeleton) immédiatement dans le handler, puis mettre à jour avec les données réelles. Le paint du spinner compte comme "next paint" et fait chuter l'INP.

INP ≠ performance perçue globale : INP mesure la pire interaction. Vous pouvez avoir 99 interactions fluides et une seule à 600ms (par exemple un filtre complexe sur un dashboard), et votre INP sera 600ms. Priorisez les interactions fréquentes ET lentes, pas uniquement les pires en valeur absolue si elles concernent 0.1% des sessions.

Single Page Applications : sur une SPA, les navigations "soft" (changement de route sans rechargement) ne sont pas encore mesurées par INP dans CrUX (la spec évolue). Mais les interactions pendant ces navigations le sont. Si votre transition de page bloque le main thread pendant 500ms, un clic utilisateur pendant ce temps aura un input delay de 500ms. Le choix entre SSR et CSR a donc un impact indirect mais réel sur INP.

INP est la métrique Core Web Vitals la plus difficile à optimiser parce qu'elle dépend du comportement réel des utilisateurs, pas d'un chargement initial mesurable en lab. L'approche gagnante combine un RUM fin avec attribution, des corrections chirurgicales sur les interactions identifiées, et un monitoring continu qui détecte les régressions avant qu'elles ne polluent 28 jours de données CrUX. Si vous n'avez qu'une action à retenir : déployez web-vitals/attribution en production cette semaine. Sans données d'attribution, vous optimisez à l'aveugle.

Articles connexes

Performance5 avril 2026

Core Web Vitals : impact réel sur le classement Google

Analyse technique des Core Web Vitals (LCP, CLS, INP) et leur influence mesurable sur le ranking Google. Données, cas concrets et optimisations.

Performance5 avril 2026

LCP lent : diagnostiquer et corriger le Largest Contentful Paint

Techniques concrètes pour diagnostiquer et corriger un LCP lent : images, fonts, TTFB, preload, CDN. Exemples de code et scénarios réels.

Performance5 avril 2026

CLS : identifier et éliminer les décalages de layout

Diagnostic technique et correction du Cumulative Layout Shift : causes réelles, outils de mesure, snippets de code et stratégies pour les sites à fort volume.