Optimiser INP en 2026 : Diagnostiquer et Réduire l'Interaction to Next Paint Sous 200 ms
L'INP mesure la latence entre une interaction et le prochain rendu. Pour passer sous 200 ms en 2026, maîtrisez Long Animation Frames, scheduler.yield, useTransition et les Web Workers. Guide pratique avec code et workflow d'audit.
L'INP (Interaction to Next Paint) est la Core Web Vital qui mesure, en millisecondes, le délai entre une interaction utilisateur (clic, tap, frappe clavier) et le prochain rendu visible que le navigateur produit en réponse. Depuis qu'il a remplacé le FID en mars 2024, c'est devenu la métrique d'expérience utilisateur la plus difficile à dompter, non pas parce que les outils manquent, mais parce qu'elle expose chaque microseconde passée à bloquer le thread principal. Pour passer sous le seuil "Bon" de 200 ms en 2026, il faut comprendre ce qui se passe entre le pointerdown et le requestAnimationFrame suivant. Cet article démonte chaque étape et donne les leviers concrets que j'utilise sur mes audits clients depuis deux ans.
INP "Bon" = ≤ 200 ms au 75e percentile ; "À améliorer" = 200–500 ms ; "Mauvais" = > 500 ms (seuils inchangés depuis 2024).
La Long Animation Frames API (stabilisée dans Chrome 123+) expose les scripts qui bloquent le thread principal pendant ≥ 50 ms, avec stack traces complètes.
scheduler.yield() (disponible sans flag depuis Chrome 129) permet de céder au thread principal entre tâches sans perdre la priorité de continuation.
Trois leviers réduisent l'INP dans 90 % des cas : céder pendant les handlers, déférer le rendu non-critique avec useTransition, et déplacer le travail CPU dans un Web Worker.
Mesurer en production via web-vitals.js v4 : l'attribution intégrée pointe directement vers le sélecteur de l'élément qui a déclenché la plus mauvaise interaction.
Le champ longAnimationFrames de l'attribution INP (web-vitals v4.2+) remplace l'ancienne décomposition input-delay / processing / presentation par une trace continue.
Qu'est-ce que l'INP et pourquoi a-t-il remplacé le FID ?
L'INP mesure la latence de toutes les interactions sur une page, puis remonte la pire (ou la 98e percentile pour les pages avec beaucoup d'interactions). Le FID, lui, ne mesurait que la première interaction, et seulement l'input delay, c'est-à-dire le temps avant que le handler ne commence à s'exécuter. Résultat : un site pouvait afficher un FID excellent à 50 ms tout en ayant des clics qui mettaient 800 ms à produire un rendu visible, parce que le travail post-handler n'était jamais mesuré.
L'INP corrige ça en mesurant l'intégralité du cycle : input delay (temps avant le handler), processing time (exécution des callbacks), puis presentation delay (temps entre la fin du dernier handler et le prochain paint). C'est la métrique qui correspond le plus directement à la perception "j'ai cliqué, est-ce que ça a réagi ?". D'après les recommandations officielles de web.dev sur INP, environ un site sur quatre échoue encore au seuil "Bon" en 2026, alors que la majorité passaient le FID sans effort.
Notez que l'INP ne se déclenche que sur trois types d'événements : click, tap (équivalent pointerup tactile) et keydown/keyup. Le scroll et le hover sont exclus, parce que leurs latences sont gérées par le compositor thread et ne reflètent pas le travail JS bloquant.
Quel est un bon score INP en 2026 ?
Les seuils, fixés par l'équipe Chrome et inchangés depuis l'introduction de la métrique :
Classement
Seuil INP (p75)
Ce que ça veut dire
Bon
≤ 200 ms
Les interactions paraissent quasi instantanées. Objectif requis pour un score Core Web Vitals vert.
À améliorer
200 ms – 500 ms
Latence perceptible. L'utilisateur sent que "ça lague" sur les pages denses en JS.
Mauvais
> 500 ms
Frustration mesurable. Sur mobile, c'est typiquement le symptôme d'un main thread saturé par l'hydration ou un framework lourd.
Un détail souvent ignoré : le seuil de 200 ms est mesuré au 75e percentile sur 28 jours glissants via le Chrome User Experience Report (CrUX). Une seule interaction lente isolée ne suffit pas à faire passer l'origine en rouge. Mais si 25 % de vos utilisateurs touchent un menu qui prend 250 ms à s'ouvrir, vous échouez. C'est pourquoi la mesure synthétique (Lighthouse) ne suffit jamais. Il faut du real user monitoring.
Comment mesurer l'INP en production avec web-vitals.js
La bibliothèque web-vitals (version 4.2+ en 2026) est l'outil de référence pour capturer l'INP côté client avec attribution. L'attribution est ce qui change tout : sans elle, vous savez qu'un INP de 380 ms s'est produit. Avec elle, vous savez quel élément a été cliqué, quelle long animation frame l'a causé, et quel script y a contribué. Honnêtement, c'est la fonctionnalité qui m'a fait économiser le plus de temps en debugging cette année.
Quelques réglages importants : reportAllChanges: false ne signale que la pire interaction quand l'utilisateur quitte la page, ce qui réduit la volumétrie. Pour du debug, mettez-le à true et vous verrez chaque interaction défiler dans la console. Si vous utilisez Datadog, Sentry ou un RUM custom, exposez au minimum les champs selector et scripts[].sourceURL. C'est ce qui permet de retrouver le coupable en 30 secondes plutôt qu'en deux heures.
Côté backend, agrégez par (url, selector), pas par URL seule. Une page peut avoir un excellent INP médian et un menu mobile catastrophique caché derrière le p99. L'agrégation par sélecteur le rend visible.
Long Animation Frames API : diagnostiquer ce qui bloque
L'API Long Animation Frames (LoAF) est stable dans tous les navigateurs Chromium depuis Chrome 123, et c'est l'outil le plus important arrivé pour l'INP. Là où l'ancienne longtask API se contentait de signaler "quelque chose a bloqué pendant 50+ ms", LoAF décompose la frame entière : rendu, style, layout, paint, et surtout la liste des scripts exécutés avec leur invoker, leur URL source et leur durée.
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration < 50) continue;
console.group(`LoAF ${entry.duration.toFixed(0)}ms @ ${entry.startTime.toFixed(0)}`);
console.log('renderStart:', entry.renderStart - entry.startTime, 'ms');
console.log('styleAndLayoutStart:', entry.styleAndLayoutStart - entry.startTime, 'ms');
for (const script of entry.scripts) {
console.log(
` ${script.invokerType} ${script.invoker}`,
`dur=${script.duration}ms`,
`forcedStyleAndLayout=${script.forcedStyleAndLayoutDuration}ms`,
`src=${script.sourceURL}:${script.sourceFunctionName}`
);
}
console.groupEnd();
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
Le champ forcedStyleAndLayoutDuration est de l'or pur : il révèle les layout thrashings (un script qui lit offsetHeight juste après avoir muté le DOM, forçant un reflow synchrone). C'était auparavant invisible sans une trace DevTools. Dans la documentation Chrome sur Long Animation Frames, l'équipe Performance recommande d'utiliser LoAF comme première étape de tout diagnostic INP, avant même d'ouvrir le panneau Performance.
Pourquoi mon INP est-il élevé ? Les causes principales
Sur les centaines de profils que j'ai analysés, 90 % des INP > 200 ms tombent dans cinq catégories. Reconnaître la signature dans une trace LoAF prend dix secondes une fois qu'on les a vues.
1. Handlers événementiels trop longs
Un onClick qui appelle une fonction de tri sur 5 000 lignes, ou qui sérialise un état Redux entier en JSON. Signature LoAF : un seul script.invoker de 300+ ms.
2. Rendu de framework non-déféré
React, Vue ou Svelte qui re-render une grande sous-arborescence en réponse à un setState synchrone. Signature : script.invokerType = "user-callback" suivi d'un long render dans le panneau Performance.
3. Hydration tardive
Un composant n'est pas hydraté quand l'utilisateur clique, donc le navigateur doit charger et exécuter le bundle JS avant que le handler ne réponde. Typique des pages Next.js sans islands ou sans useTransition autour de l'hydration. Pour réduire le coût d'hydration, l'optimisation du bundle JavaScript avec code splitting et tree shaking reste le levier le plus rentable.
4. Layout thrashing
Code legacy qui mesure puis modifie le DOM en boucle. forcedStyleAndLayoutDuration > 0 dans la LoAF.
5. Scripts tiers
Tag manager, A/B testing, chat widgets. Leur script.sourceURL pointe vers un domaine externe ; leur invokerType est souvent 'observer-callback' (mutation observer) ou 'window.requestAnimationFrame'.
Réduire le travail bloquant avec scheduler.yield()
Le pattern historique pour céder au thread principal, await new Promise(r => setTimeout(r, 0)), est trompeur : le callback est planifié comme une tâche normale, donc d'autres tâches haute priorité peuvent passer devant et l'INP empire. scheduler.yield(), disponible sans flag dans Chrome depuis la version 129 (septembre 2024) et désormais dans Edge et Opera, résout ça : il cède le thread, mais la continuation conserve une priorité haute pour reprendre dès que possible.
async function processLargeList(items) {
for (let i = 0; i < items.length; i++) {
doExpensiveWork(items[i]);
// Tous les 50 items OU si on a dépassé 50ms, on cède
if (i % 50 === 0 && shouldYield()) {
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
// Fallback Safari / anciens navigateurs
await new Promise(r => setTimeout(r, 0));
}
}
}
}
let lastYield = performance.now();
function shouldYield() {
if (performance.now() - lastYield > 50) {
lastYield = performance.now();
return true;
}
return false;
}
La règle des 50 ms vient directement du seuil LoAF : si vous cédez avant 50 ms, vous ne produirez pas de long animation frame, donc pas de pénalité INP. Pour les handlers d'événements, utilisez le pattern yield-then-react. Faites le minimum dans le handler synchrone (juste assez pour donner un feedback visuel, comme désactiver un bouton ou afficher un spinner), puis await scheduler.yield() avant le travail lourd. Le navigateur peint l'état "loading" immédiatement et l'INP reste sous 200 ms même si le calcul prend 2 secondes au total. J'ai vu cette astuce seule faire passer une page d'export PDF de 1200 ms à 90 ms d'INP, sans toucher au calcul lui-même.
Déléguer le travail lourd aux Web Workers
Quand le travail dépasse les quelques centaines de millisecondes, comme un parsing de gros JSON, des calculs cryptographiques, la manipulation d'images côté client ou une recherche full-text, aucun nombre de yield() ne sauvera l'INP. Le travail doit quitter le thread principal. Un Web Worker dédié, avec Comlink pour cacher la complexité du postMessage, est la solution canonique.
// worker.js
import * as Comlink from 'comlink';
const api = {
parseAndSort(json) {
const data = JSON.parse(json); // hors thread principal
return data.sort((a, b) => b.score - a.score);
},
};
Comlink.expose(api);
// main.js
import * as Comlink from 'comlink';
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
const api = Comlink.wrap(worker);
button.addEventListener('click', async () => {
button.disabled = true;
const sorted = await api.parseAndSort(rawJson); // INP reste bas
renderResults(sorted);
button.disabled = false;
});
Attention à deux écueils : (1) la sérialisation postMessage a un coût, donc évitez de transférer des arbres d'objets gigantesques en boucle et préférez des Transferable objects (ArrayBuffer) quand c'est possible ; (2) la création d'un worker prend 5 à 15 ms sur mobile, donc utilisez un pool partagé plutôt que d'en spawner un par interaction.
React 19, useTransition et hydration progressive
React 19 (stable depuis décembre 2024) a transformé le terrain pour l'INP. Deux primitives sont essentielles : useTransition et useDeferredValue. Une transition indique à React que la mise à jour d'état est non-urgente. Les autres événements (frappe clavier, autre clic) peuvent l'interrompre, et le rendu peut être réparti sur plusieurs frames.
Le input reste fluide même si filterExpensive prend 200 ms, parce que React met en pause son travail dès qu'un nouvel événement arrive. C'est exactement ce que mesure l'INP : la latence jusqu'au prochain paint, pas la durée totale du travail. Pour Next.js 15, les Server Components réduisent l'hydration de la majorité de la page à zéro. Le seul JS qui s'exécute côté client est celui des Client Components explicitement marqués. Combiné avec des Server Actions, on évite les longues callbacks fetch+render qui plombaient le pattern SSR classique.
Auditer l'INP dans Lighthouse et DevTools
Workflow d'audit en 2026, dans l'ordre que j'applique systématiquement :
CrUX dashboard ou PageSpeed Insights pour confirmer l'INP réel (75e percentile). Lighthouse local mesure un INP synthétique via simulation, utile pour comparer deux versions, pas pour valider un seuil.
web-vitals attribution en RUM pour identifier le sélecteur et l'URL des pages les plus lentes.
DevTools Performance panel avec throttling CPU 4× et "Slow 4G". C'est le profil moyen d'un utilisateur mobile en 2026 selon les données HTTP Archive.
Filtrer les Long animation frames dans le panneau Performance (case à cocher "Long animation frames" dans la timeline). Les frames > 50 ms sont surlignées en orange.
Cliquer sur une LoAF pour voir le flame chart du script responsable, sa pile d'appels et son forcedStyleAndLayoutDuration.
Le FID ne mesurait que le délai avant exécution du premier handler de la session. L'INP mesure la latence complète (input delay + processing + presentation) de toutes les interactions, puis remonte la pire. C'est pourquoi une page avec un FID excellent peut avoir un INP catastrophique : le FID ignorait le travail post-handler et le rendu.
Quel est un bon score INP ?
Un INP ≤ 200 ms au 75e percentile sur 28 jours est considéré comme "Bon" par Google. Entre 200 et 500 ms, "À améliorer" ; au-delà de 500 ms, "Mauvais". Les seuils sont restés stables depuis 2024.
L'INP affecte-t-il le SEO ?
Oui. L'INP fait partie des trois Core Web Vitals utilisées dans le signal "page experience" de Google Search depuis mars 2024. Une origine qui échoue au seuil "Bon" perd ce signal positif dans son ranking, particulièrement sur les pages où l'engagement utilisateur est important.
Comment l'INP est-il calculé sur une page avec beaucoup d'interactions ?
Sur une page avec moins de 50 interactions, l'INP est simplement la pire interaction. Au-delà, Chrome utilise approximativement la 98e percentile : il ignore l'interaction la pire toutes les 50 interactions, pour ne pas qu'un seul événement aberrant détermine la métrique de toute la session.
Le scroll ou le hover comptent-ils dans l'INP ?
Non. Seuls les clics, les taps tactiles et les frappes clavier (keydown/keyup) sont mesurés. Le scroll est géré par le compositor thread du navigateur, et le hover ne produit pas systématiquement de rendu visible.
Historique de l'article (1)
— SEO meta refreshed (title and description updated)
Réduisez votre bundle JS avec Vite en 2026. Diagnostic avec rollup-plugin-visualizer, code splitting, tree shaking, remplacement des dépendances lourdes, compression Brotli et budget de performance CI avec du code prêt à l'emploi.
Apprenez à orchestrer HTTP Cache-Control, Service Worker avec Workbox 7 et CDN edge pour un cache multi-couches performant. Configurations serveur, stratégies par type de contenu et monitoring inclus.
En 2026, les images représentent 60 à 70 % du poids des pages web. Ce guide couvre tout : formats AVIF/WebP, images responsives, lazy loading, pipelines Sharp et négociation de contenu serveur. Avec du code prêt à l'emploi.