Introduction : Pourquoi l'INP reste le cauchemar des développeurs en 2026
Soyons honnêtes : en 2026, l'INP est devenu la bête noire des équipes frontend. 43 % des sites web échouent encore au seuil de 200 ms fixé par Google pour l'Interaction to Next Paint. C'est le taux d'échec le plus élevé parmi les trois Core Web Vitals — et de loin. Le LCP ? Maîtrisé. Le CLS ? Sous contrôle. Mais l'INP, lui, continue de résister.
Et franchement, c'est logique.
Contrairement au First Input Delay (FID) qu'il a remplacé en mars 2024, l'INP ne se contente pas de mesurer le délai de la première interaction. Il observe toutes les interactions de l'utilisateur pendant sa visite — chaque clic, chaque appui sur un bouton, chaque saisie clavier — et retient la plus lente. C'est un peu comme si on vous jugeait non pas sur votre meilleur examen, mais sur le pire. Autant dire que ça ne pardonne pas.
Dans ce guide, on va décortiquer les trois phases de l'INP, explorer les techniques concrètes pour débloquer le thread principal JavaScript (y compris la fameuse API scheduler.yield()), et voir comment diagnostiquer les interactions lentes avec la Long Animation Frames API. Du code qui fonctionne, des résultats mesurables, zéro bla-bla théorique.
Comprendre les Trois Phases de l'INP
Avant de vouloir optimiser quoi que ce soit, il faut comprendre ce que l'INP mesure exactement. Chaque interaction se décompose en trois phases bien distinctes — et c'est souvent là que le diagnostic commence.
Phase 1 : Le Délai d'Entrée (Input Delay)
C'est le temps entre le moment où l'utilisateur clique (ou touche, ou tape au clavier) et le moment où le navigateur commence enfin à exécuter les gestionnaires d'événements associés. Ce délai survient quand le thread principal est déjà occupé — typiquement par l'exécution d'un autre script JavaScript qui traîne en longueur.
Un chiffre qui parle : les interactions pendant la phase de chargement affichent un INP au 75e percentile de 132 ms, contre seulement 50 ms après le chargement complet. C'est un facteur 2,6x. Le chargement initial est clairement le moment le plus critique (et souvent le plus négligé).
Phase 2 : Le Temps de Traitement (Processing Time)
C'est la durée d'exécution des gestionnaires d'événements eux-mêmes — vos callbacks onclick, onkeydown, oninput, etc. Un gestionnaire qui fait des calculs lourds, manipule un DOM complexe ou déclenche des requêtes synchrones va directement allonger cette phase.
Phase 3 : Le Délai de Présentation (Presentation Delay)
Une fois les gestionnaires exécutés, le navigateur doit recalculer les styles, effectuer le layout et peindre les pixels modifiés à l'écran. Un DOM volumineux ou des modifications CSS coûteuses peuvent considérablement rallonger cette étape. C'est la phase qu'on a tendance à oublier.
L'INP final, c'est la somme de ces trois phases pour l'interaction la plus lente de la session. Google considère un INP inférieur ou égal à 200 ms comme « bon », entre 200 et 500 ms comme « à améliorer », et au-delà de 500 ms comme « mauvais ».
Technique 1 : Découper les Longues Tâches avec scheduler.yield()
La cause numéro un d'un mauvais INP ? Les longues tâches JavaScript. Celles qui bloquent le thread principal pendant plus de 50 ms. Pendant qu'une tâche longue s'exécute, le navigateur ne peut tout simplement pas répondre aux interactions utilisateur. Point.
L'API scheduler.yield() est, à mon avis, la solution la plus élégante à ce problème en 2026. Elle permet de couper une tâche longue en morceaux tout en conservant la priorité d'exécution — contrairement à setTimeout qui envoie la suite de votre code en fin de file d'attente.
Différence Clé : scheduler.yield() vs setTimeout
Avec setTimeout(fn, 0), le navigateur place votre callback en fin de la file des tâches. Si des scripts tiers (analytics, publicités, widgets) ont ajouté des tâches entre-temps, votre code doit attendre son tour. Résultat : l'interaction peut rester bloquée bien plus longtemps que nécessaire.
Avec scheduler.yield(), c'est différent. Le navigateur cède le contrôle pour traiter les interactions en attente, puis reprend votre code en priorité. C'est exactement ce qu'il faut pour l'INP.
// ❌ Problème : une tâche longue qui bloque le thread principal
function handleClick() {
updateUI(); // 30ms
recalculateLayout(); // 40ms
sendAnalytics(); // 25ms
// Total : 95ms — le navigateur est bloqué pendant toute la durée
}
// ✅ Solution : découper avec scheduler.yield()
async function handleClick() {
updateUI(); // 30ms — exécution immédiate
await scheduler.yield(); // Cède le contrôle — les interactions en attente sont traitées
recalculateLayout(); // 40ms — reprend en priorité
await scheduler.yield();
sendAnalytics(); // 25ms — reprend en priorité
}
Compatibilité Navigateur et Fallback
En février 2026, scheduler.yield() est supporté par Chrome (depuis 2024) et Firefox (depuis août 2025). Safari ne le supporte toujours pas — ce qui ne surprendra personne. Il est donc indispensable de prévoir un fallback :
// Fonction utilitaire avec fallback automatique
async function yieldToMain() {
if (globalThis.scheduler?.yield) {
return scheduler.yield();
}
// Fallback : setTimeout pour les navigateurs sans support
return new Promise(resolve => setTimeout(resolve, 0));
}
// Utilisation dans un traitement par lots
async function processLargeDataset(items) {
const BATCH_SIZE = 50;
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE);
batch.forEach(item => processItem(item));
await yieldToMain(); // Cède après chaque lot
}
}
Technique 2 : Optimiser les Gestionnaires d'Événements
Le temps de traitement — la deuxième phase de l'INP — dépend directement de ce qui se passe dans vos gestionnaires d'événements. Et c'est souvent là qu'on perd le plus de temps sans s'en rendre compte.
Séparer le Travail Visible du Travail Différable
Voilà l'idée clé : dans un gestionnaire d'événements, tout n'est pas urgent. L'astuce, c'est d'identifier ce qui doit être affiché immédiatement à l'utilisateur (le feedback visuel) et ce qui peut attendre un peu (analytics, synchronisation, mises à jour secondaires).
// ❌ Tout dans le même callback — bloque le thread
button.addEventListener('click', () => {
updateCartUI(); // Visible — urgent
recalculateTotals(); // Visible — urgent
sendAnalyticsEvent(); // Invisible — peut attendre
syncWithServer(); // Invisible — peut attendre
updateRecommendations(); // Secondaire — peut attendre
});
// ✅ Séparer l'urgent du différable
button.addEventListener('click', async () => {
// Phase 1 : feedback visuel immédiat
updateCartUI();
recalculateTotals();
// Phase 2 : céder le contrôle, puis travail non visible
await yieldToMain();
sendAnalyticsEvent();
syncWithServer();
// Phase 3 : travail secondaire quand le navigateur est inactif
requestIdleCallback(() => {
updateRecommendations();
});
});
Débouncer et Throttler les Événements Fréquents
Les événements comme scroll, resize, input et mousemove se déclenchent des dizaines de fois par seconde. Sans contrôle, chaque déclenchement crée du travail sur le thread principal qui s'accumule — et ça finit par exploser l'INP.
// Debounce simple pour les champs de recherche
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// Throttle pour les événements de scroll
function throttle(fn, limit) {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => { inThrottle = false; }, limit);
}
};
}
// Application
searchInput.addEventListener('input', debounce(handleSearch, 300));
window.addEventListener('scroll', throttle(handleScroll, 100), { passive: true });
Utiliser les Listeners Passifs
Pour les événements scroll et touch, ajouter { passive: true } indique au navigateur que votre gestionnaire n'appellera jamais preventDefault(). Résultat : le navigateur peut commencer le défilement immédiatement sans attendre la fin de l'exécution du callback. C'est une ligne de code, et le gain est instantané.
Technique 3 : Décharger le Thread Principal avec les Web Workers
Parfois, il n'y a pas de raccourci : certains calculs sont intrinsèquement lourds. Tri de grands tableaux, transformations de données, traitement d'images, calculs mathématiques complexes… Plutôt que de bloquer le thread principal (et donc l'utilisateur), déportez tout ça dans un Web Worker — un thread séparé qui n'interfère absolument pas avec les interactions.
// worker.js — s'exécute dans un thread séparé
self.addEventListener('message', (event) => {
const { data, sortField, filterCriteria } = event.data;
// Calculs lourds sans bloquer le thread principal
const filtered = data.filter(item => item.category === filterCriteria);
const sorted = filtered.sort((a, b) => a[sortField] - b[sortField]);
const stats = {
total: sorted.length,
average: sorted.reduce((sum, item) => sum + item.value, 0) / sorted.length
};
self.postMessage({ sorted, stats });
});
// main.js — thread principal, reste réactif
const worker = new Worker('/js/worker.js');
button.addEventListener('click', () => {
// Feedback visuel immédiat
showLoadingSpinner();
// Envoyer le travail lourd au Worker
worker.postMessage({
data: largeDataset,
sortField: 'price',
filterCriteria: 'electronics'
});
});
worker.addEventListener('message', (event) => {
const { sorted, stats } = event.data;
hideLoadingSpinner();
renderResults(sorted);
updateStats(stats);
});
Le résultat est immédiat : en déportant les calculs dans un Worker, le thread principal reste entièrement disponible pour répondre aux interactions. L'utilisateur ne perçoit aucune latence, même si le traitement prend plusieurs centaines de millisecondes en arrière-plan. D'expérience, c'est souvent la technique qui impressionne le plus en démo.
Technique 4 : Réduire la Taille du DOM
Un DOM volumineux allonge la troisième phase de l'INP — le fameux délai de présentation. Chaque modification du DOM déclenche un recalcul des styles et un re-layout, et le coût augmente proportionnellement au nombre de nœuds. Plus vous avez de nœuds, plus chaque interaction coûte cher au navigateur.
Les seuils recommandés :
- Moins de 1 500 nœuds au total
- Profondeur maximale de 32 niveaux
- Aucun élément parent avec plus de 60 enfants
Si vous dépassez ces seuils (et soyons réalistes, beaucoup de sites les dépassent), c'est un signal d'alarme.
Virtualisation de Listes
Si votre page affiche des centaines d'éléments — tableau de données, catalogue produits, fil d'actualités — la virtualisation permet de ne rendre que les éléments visibles à l'écran :
// Principe de la virtualisation simplifiée
function renderVisibleItems(container, allItems, itemHeight) {
const scrollTop = container.scrollTop;
const viewportHeight = container.clientHeight;
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(viewportHeight / itemHeight) + 1,
allItems.length
);
// Ne rendre que les éléments visibles
const fragment = document.createDocumentFragment();
for (let i = startIndex; i < endIndex; i++) {
const el = createItemElement(allItems[i]);
el.style.transform = `translateY(${i * itemHeight}px)`;
fragment.appendChild(el);
}
container.innerHTML = '';
container.style.height = `${allItems.length * itemHeight}px`;
container.appendChild(fragment);
}
Bon, cet exemple est volontairement simplifié. En production, utilisez plutôt une bibliothèque éprouvée comme @tanstack/virtual (anciennement react-virtual) ou lit-virtualizer. Pas la peine de réinventer la roue ici.
Éviter le Layout Thrashing
Le « layout thrashing », c'est quand vous alternez lectures et écritures de propriétés DOM dans une boucle. Chaque lecture force le navigateur à recalculer le layout avant de répondre — et ça peut vite devenir catastrophique :
// ❌ Layout thrashing — force un recalcul à chaque itération
elements.forEach(el => {
const height = el.offsetHeight; // Lecture → force un layout
el.style.height = height * 2 + 'px'; // Écriture → invalide le layout
});
// ✅ Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // Toutes les lectures d'abord
elements.forEach((el, i) => {
el.style.height = heights[i] * 2 + 'px'; // Toutes les écritures ensuite
});
Technique 5 : Maîtriser les Scripts Tiers
Ah, les scripts tiers. Analytics, widgets de chat, bannières de consentement, outils de personnalisation… Ils sont souvent les premiers responsables d'un mauvais INP. Et le problème, c'est qu'ils s'exécutent sur votre thread principal, ajoutent des gestionnaires d'événements et déclenchent des longues tâches — sans que vous ayez le moindre contrôle sur leur code.
Stratégie de Chargement Différé
<!-- ❌ Chargement immédiat — bloque le thread principal -->
<script src="https://cdn.chatwidget.io/widget.js"></script>
<!-- ✅ Chargement différé après l'interaction utilisateur -->
<script>
// Charger le widget de chat seulement quand l'utilisateur en a besoin
function loadChatWidget() {
const script = document.createElement('script');
script.src = 'https://cdn.chatwidget.io/widget.js';
document.head.appendChild(script);
}
// Déclencher au premier signe d'engagement
const triggers = ['scroll', 'mousemove', 'touchstart', 'keydown'];
function onFirstInteraction() {
loadChatWidget();
triggers.forEach(event =>
window.removeEventListener(event, onFirstInteraction)
);
}
triggers.forEach(event =>
window.addEventListener(event, onFirstInteraction, { once: true, passive: true })
);
</script>
Auditer l'Impact des Scripts Tiers
Ouvrez Chrome DevTools, onglet Performance, et enregistrez une session. Cherchez les barres jaunes qui dépassent 50 ms dans le flame chart — ce sont vos coupables. Si un script tiers dégrade votre INP de manière significative, vous avez trois options : le charger dans un iframe isolé, le remplacer par une alternative plus légère, ou (option radicale mais parfois nécessaire) le supprimer purement et simplement.
Diagnostiquer l'INP avec la Long Animation Frames API (LoAF)
Savoir que votre INP est mauvais, c'est une chose. Comprendre pourquoi, c'est une autre histoire. Et c'est exactement là que la Long Animation Frames API (LoAF) entre en jeu, disponible depuis Chrome 123.
Là où l'INP vous montre l'effet (« cette interaction a pris 350 ms »), LoAF vous montre la cause : quel script s'est exécuté, pendant combien de temps, et dans quelle phase de l'interaction. Honnêtement, c'est un game changer pour le debugging.
Collecter les Données LoAF en Production
La méthode la plus simple consiste à utiliser la bibliothèque web-vitals (v4+) qui inclut automatiquement les entrées LoAF dans les données d'attribution de l'INP :
import { onINP } from 'web-vitals/attribution';
onINP((metric) => {
console.log('INP:', metric.value, 'ms');
// Attribution détaillée
const attribution = metric.attribution;
console.log('Élément interactif:', attribution.interactionTarget);
console.log('Type d\'interaction:', attribution.interactionType);
// Phases de l'interaction
console.log('Input Delay:', attribution.inputDelay, 'ms');
console.log('Processing Time:', attribution.processingDuration, 'ms');
console.log('Presentation Delay:', attribution.presentationDelay, 'ms');
// Scripts responsables (via LoAF)
attribution.longAnimationFrameEntries?.forEach(loaf => {
loaf.scripts?.forEach(script => {
console.log('Script:', script.sourceURL);
console.log('Durée:', script.duration, 'ms');
console.log('Type:', script.invokerType);
});
});
});
Arbre de Décision pour le Diagnostic
Une fois l'interaction problématique identifiée, voici comment procéder :
- Input Delay élevé ? → Un autre script bloque le thread principal au moment de l'interaction. Identifiez-le avec LoAF et découpez-le ou différez-le.
- Processing Time élevé ? → Vos propres gestionnaires d'événements sont trop lents. Optimisez-les, déportez les calculs dans un Worker, ou utilisez
scheduler.yield(). - Presentation Delay élevé ? → Le DOM est trop volumineux ou les modifications CSS sont trop coûteuses. Réduisez le nombre de nœuds et éliminez le layout thrashing.
Mesurer l'INP : Outils et Méthodologie
Données de Terrain (Field Data)
Les données de terrain — issues de vrais utilisateurs dans des conditions réelles — sont indispensables pour mesurer l'INP de manière fiable. On ne le dira jamais assez : les données labo seules ne suffisent pas.
- Chrome User Experience Report (CrUX) : données agrégées de millions d'utilisateurs Chrome, accessibles via PageSpeed Insights ou BigQuery
- Bibliothèque web-vitals : mesure JavaScript côté client, à intégrer dans votre solution RUM (Real User Monitoring)
- DebugBear, SpeedCurve, RUMvision : plateformes de monitoring avec support natif de l'INP et de LoAF
Données de Laboratoire (Lab Data)
Les outils de labo ne capturent pas toujours l'INP de manière représentative (ils simulent un nombre limité d'interactions), mais ils restent utiles pour le débogage :
- Chrome DevTools — onglet Performance : enregistrez une session, interagissez avec la page, et analysez les longues tâches dans le flame chart
- Lighthouse : à partir de la version 12, Lighthouse inclut des métriques de réactivité améliorées
- Web Vitals Extension : affiche l'INP de chaque interaction en temps réel dans le navigateur — très pratique pour les tests rapides
Résultats Concrets : Études de Cas 2025-2026
L'optimisation de l'INP n'est pas qu'un exercice technique — elle a un impact business très concret. Quelques exemples qui parlent :
- redBus : +7 % de ventes et une amélioration de 80 à 100 % des taux de conversion mobile après optimisation de l'INP
- BookBetter (Accesto) : réduction de 90 % du temps d'INP grâce à un refactoring ciblé des gestionnaires d'événements
- Banque sécurisée (Akamai) : passage de 55 % à 87 % d'utilisateurs avec un INP « bon » (sous 200 ms) grâce à l'optimisation des scripts de Bot Manager
- Site e-commerce haute fréquentation : INP réduit à 50 ms au P75 et baisse de 60 % des tickets de support liés à la performance
Ces chiffres ne mentent pas. Globalement, la part des sites avec un « bon » INP est passée de 57 % en mars 2022 à 76 % en 2025. L'écart reste cependant significatif selon les régions — les pays avec une prédominance de smartphones d'entrée de gamme affichent des scores INP nettement plus élevés. C'est un rappel important : optimisez toujours pour les appareils les plus modestes de votre audience.
Checklist Récapitulative : Optimiser l'INP en 2026
Allez, on résume. Voici les techniques couvertes dans ce guide, classées par impact :
- Impact élevé : Découper les longues tâches avec
scheduler.yield()— Différer les scripts non critiques et le code tiers — Utiliser les Web Workers pour les calculs lourds — Implémenter le code splitting et les imports dynamiques - Impact moyen : Débouncer/throttler les événements fréquents — Réduire la taille du DOM sous 1 500 nœuds — Utiliser les listeners passifs pour scroll et touch — Virtualiser les longues listes
- Impact ciblé : Éviter le layout thrashing (batch reads/writes) — Charger les scripts tiers après la première interaction — Monitorer l'INP en continu avec web-vitals + LoAF
FAQ : Questions Fréquentes sur l'INP
Quelle est la différence entre l'INP et le FID ?
Le FID (First Input Delay) ne mesurait que le délai d'entrée de la première interaction. L'INP, lui, mesure les trois phases (délai d'entrée, traitement et présentation) de toutes les interactions et retient la plus lente. C'est une évaluation beaucoup plus fidèle de la réactivité réelle perçue par l'utilisateur.
Comment savoir si mon INP est mauvais à cause du JavaScript ou du DOM ?
Utilisez la bibliothèque web-vitals avec attribution pour décomposer l'INP en ses trois phases. Si l'Input Delay ou le Processing Time dominent, le problème est côté JavaScript (tâches longues, gestionnaires lents). Si c'est le Presentation Delay qui ressort, regardez du côté du DOM et du CSS — trop de nœuds, layout thrashing, recalculs de styles inutiles.
scheduler.yield() est-il supporté par tous les navigateurs ?
Pas encore. En février 2026, scheduler.yield() est supporté par Chrome (depuis 2024) et Firefox (depuis août 2025). Safari traîne encore. Prévoyez toujours un fallback avec setTimeout(resolve, 0) encapsulé dans une Promise — c'est moins performant mais ça assure la compatibilité universelle.
Quel est l'impact de l'INP sur le référencement Google ?
L'INP fait partie des Core Web Vitals, qui sont un facteur de classement confirmé par Google. Les sites dont l'INP dépasse 200 ms risquent de perdre des positions dans les résultats de recherche, surtout sur mobile. Et au-delà du SEO pur, un mauvais INP fait grimper le taux de rebond : chaque 100 ms de délai supplémentaire peut réduire les conversions de 7 %.
Faut-il optimiser l'INP sur desktop et mobile séparément ?
Oui, absolument. Les données montrent qu'un site typique gère une interaction en 50 ms sur desktop mais en 100 ms sur mobile au 75e percentile. Les appareils mobiles d'entrée de gamme amplifient considérablement l'impact des longues tâches JavaScript. Testez et optimisez toujours en priorité pour les conditions mobiles réelles — c'est là que vos utilisateurs souffrent le plus.