Avancé 6 min 1 min de lecture Core Web Vitals

Optimiser les longues tâches JavaScript pour l’INP

Découpez vos longues tâches JavaScript pour libérer le thread principal, accélérer les interactions et améliorer durablement votre INP.

Par Nicolas Dupont Mis à jour le 23 juin 2026

Une tâche est une unité de travail traitée par le navigateur : analyse du HTML et du CSS, exécution du JavaScript, rendu. Le thread principal ne peut en traiter qu’une à la fois, et toute tâche qui dépasse 50 millisecondes est considérée comme une longue tâche. La durée au-delà de ces 50 millisecondes constitue sa période de blocage. Pendant qu’une tâche s’exécute, le navigateur ne peut pas répondre aux interactions : si elles s’accumulent, l’interface paraît lente, voire figée.

Découper une longue tâche en plusieurs tâches plus courtes est donc essentiel pour l’INP (Interaction to Next Paint). En SEO, une page qui réagit vite aux clics et aux saisies offre une bien meilleure expérience et limite les abandons. Ce découpage est l’un des leviers majeurs pour optimiser l’INP. Le JavaScript suit un modèle d’exécution jusqu’à la fin : chaque tâche s’exécute entièrement, quelle que soit la durée pendant laquelle elle bloque le thread, d’où l’importance de la fractionner volontairement.

Sur le thread principal, le navigateur ne traite qu’une tâche à la fois, et toute tâche qui dépasse cinquante millisecondes bloque les interactions. Mon conseil de praticien : apprenez à découper ces longues tâches et privilégiez scheduler.yield quand c’est possible plutôt que les vieux contournements à base de setTimeout. Céder la main au bon moment fait toute la différence sur l’INP.

— Nicolas Dupont

Céder la place au thread principal

La notion centrale est la cession du thread principal : interrompre brièvement votre travail pour laisser le navigateur exécuter des tâches plus prioritaires, comme les interactions utilisateur ou les mises à jour d’interface. Une fois ces tâches traitées, votre code reprend là où il s’était arrêté. C’est ce mécanisme qui transforme une seule longue tâche bloquante en une série de petites tâches entre lesquelles le navigateur peut respirer.

La première stratégie consiste à séparer le travail critique, visible par l’utilisateur, du travail qui peut attendre. On exécute d’abord ce qui doit s’afficher, puis on diffère le reste dans une tâche distincte. Cela suffit déjà à réduire fortement le temps de blocage perçu lors d’une interaction, ce qui contribue aussi à réduire le délai d’entrée quand l’utilisateur agit pendant le chargement.

Différer manuellement avec setTimeout

La méthode historique consiste à passer une fonction à setTimeout : son rappel est alors reporté dans une tâche séparée, même avec un délai de zéro. C’est une forme de cession simple, adaptée à une série de fonctions à exécuter séquentiellement. Elle présente toutefois des limites : l’ergonomie est médiocre pour traiter de gros volumes en boucle, le navigateur impose un délai minimal de cinq millisecondes après cinq setTimeout imbriqués, et la tâche différée part en fin de file d’attente, derrière d’éventuelles tâches déjà en attente.

L’exemple ci-dessous illustre cette séparation : le travail visible s’exécute immédiatement, le reste est différé.

Cession manuelle avec setTimeout
function saveSettings () {
  // Travail critique, visible par l'utilisateur :
  validateForm();
  showSpinner();
  updateUI();

  // Travail non visible, différé dans une tâche séparée :
  setTimeout(() => {
    saveToDatabase();
    sendAnalytics();
  }, 0);
}

Privilégier scheduler.yield()

scheduler.yield() est une API conçue spécifiquement pour céder la place au thread principal. Elle renvoie une promesse résolue dans une tâche future : combinée à await, elle suspend l’exécution à cet endroit, planifie la suite dans une nouvelle tâche, puis la reprend exactement là où elle s’était arrêtée. Son atout décisif est la continuation prioritaire : la suite de votre tâche s’exécute avant les autres tâches similaires, ce qui empêche des scripts tiers de s’intercaler et de perturber l’ordre de votre code.

Comme la compatibilité n’est pas universelle, prévoyez un repli. La fonction d’aide ci-dessous utilise scheduler.yield() s’il est disponible, sinon elle revient à une cession via setTimeout enveloppé dans une promesse.

Fonction d'aide yieldToMain
function yieldToMain () {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }

  // Repli : céder avec setTimeout.
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

Découper une file de tâches par lots

Pour les traitements lourds, on insère un point de cession dans la boucle. Mais céder après chaque élément est contre-productif quand les opérations sont très courtes : la surcharge de la cession s’accumule et peut dépasser le temps de travail réel. La bonne approche consiste à ne céder que lorsqu’un délai suffisant s’est écoulé depuis la dernière cession. Le seuil courant est de 50 millisecondes, ajustable pour équilibrer réactivité et temps de traitement total. Une partie de ce coût se règle aussi en amont, en cherchant à réduire le coût de démarrage du JavaScript.

Notez bien que scheduler.yield() ne fait que planifier une tâche : c’est await qui suspend réellement l’exécution. N’oubliez pas await et méfiez-vous des méthodes comme Array.prototype.forEach, qui n’attendent pas la résolution de chaque rappel. L’exemple ci-dessous applique un lot avec une échéance de 50 millisecondes.

Découpage par lots avec échéance
async function runJobs(jobQueue, deadline=50) {
  let lastYield = performance.now();

  for (const job of jobQueue) {
    // Exécuter la tâche :
    job();

    // Au-delà de l'échéance, céder le thread principal :
    if (performance.now() - lastYield > deadline) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

Comparer les stratégies de cession

Toutes les approches ne se valent pas. Le tableau ci-dessous résume leurs caractéristiques pour vous aider à choisir.

Stratégie Atout principal Limite ou recommandation

setTimeout

Disponible partout, simple à mettre en place

Part en fin de file ; délai minimal de 5 ms après 5 imbrications

scheduler.yield()

Continuation prioritaire, ergonomie avec await

Support partiel : prévoir un repli pour les navigateurs anciens

scheduler.postTask()

Planification explicite avec niveaux de priorité

Pour des besoins de priorisation fins entre tâches

isInputPending()

Continue le travail tant qu’aucune entrée n’est en attente

Déconseillée aujourd’hui : préférez une cession systématique

N'utilisez plus isInputPending()

L’API isInputPending() permettait de ne céder que si une entrée utilisateur était en attente. Elle n’est plus recommandée : elle peut renvoyer faux à tort même après une interaction, et l’entrée n’est pas le seul motif valable de cession, car les animations et les mises à jour régulières de l’interface comptent tout autant. Préférez une cession systématique avec scheduler.yield(), plus complète et plus fiable.

Besoin d'aide pour mettre en pratique ? Nos experts SEO vous accompagnent.
Parler à un expert

À faire

  • céder le thread pour le travail critique visible
  • utiliser scheduler.yield() avec un repli
  • regrouper les cessions par lots
  • limiter le travail dans chaque fonction

À éviter

  • laisser une tâche dépasser 50 ms sans la fractionner
  • céder après chaque micro-opération
  • oublier await avant la cession
  • continuer à utiliser isInputPending()

Points clés à retenir

  • Repérer les tâches qui dépassent 50 millisecondes
  • Séparer le travail critique du travail différable
  • Préférer scheduler.yield() pour une continuation prioritaire
  • Regrouper les cessions par lots avec une échéance
  • Réduire au maximum le travail de chaque fonction

Un thread principal qui respire, c’est une page qui répond vite.

Quiz : testez vos connaissances

  1. À partir de quelle durée une tâche du thread principal est-elle considérée comme longue ?

    • Dès qu'elle dépasse 2,5 secondes
    • Dès qu'elle dépasse 200 millisecondes
    • Dès qu'elle dépasse 50 millisecondes

    Une tâche devient longue dès qu’elle dépasse 50 millisecondes. Le temps au-delà de ce seuil correspond à sa période de blocage du thread principal.

  2. Quelle API est conçue spécifiquement pour céder la place au thread principal ?

    • setInterval(), qui planifie la cession toutes les n millisecondes
    • scheduler.yield(), qui suspend l'exécution puis la reprend exactement là où elle s'était arrêtée
    • isInputPending(), qui reste la méthode recommandée pour céder le thread

    scheduler.yield() est conçue pour céder le thread : combinée à await, elle suspend l’exécution, planifie la suite dans une nouvelle tâche, puis la reprend au même point.

  3. Pourquoi céder le thread après chaque élément d'une boucle peut-il être contre-productif ?

    • Parce que la cession est interdite à l'intérieur d'une boucle
    • Parce que la surcharge de la cession s'accumule et peut dépasser le temps de travail réel quand les opérations sont très courtes
    • Parce que céder empêche définitivement le code de reprendre

    Céder après chaque micro-opération accumule la surcharge de la cession, qui peut dépasser le travail réel. Mieux vaut regrouper les cessions par lots.

À propos de l'auteur

Nicolas Dupont

Nicolas Dupont

Expert SEO Sénior & Fondateur

Il a fait grimper des sites en haut de Google et accompagné plus de 120 entreprises avant de fonder Les Webineurs. Sur votre projet SEO, vous échangez directement avec Nicolas, pas un commercial ni un junior. Un expert dédié et du trafic organique qui dure.

Voir le profil

Besoin d'un accompagnement SEO ?

Des tâches JavaScript bloquent votre interface ? Identifions ensemble celles à fractionner en priorité.

Faire le point avec un expert SEO