Report des effets secondaires dans Next.js avec l’API after()
Découvrez comment l’API after() de Next.js exécute des analyses, enregistre des journaux et gère des tâches en arrière-plan après la réponse, ainsi que ses garanties, les pièges potentiels et les compromis en matière de gestion des erreurs.
La plupart des problèmes de performance dans les applications Next.js proviennent du fait que chaque tâche est traitée comme ayant la même urgence. Les équipes font régulièrement attendre les utilisateurs pendant que des données sont enregistrées, des journaux d’audit sont créés, des caches sont purgés ou des notifications envoyées avant de leur retourner une réponse. Un visiteur doit supporter un délai de 300 millisecondes pour une insertion dans la base de données dont personne n’a réellement besoin de voir le résultat. La réponse elle-même arrive en retard parce que quelques effets secondaires non liés ont été forcés à s’exécuter les uns après les autres en ligne de commande.
Le schéma typique consiste à associer l’exécution des tâches critiques aux opérations de maintenance courante. Les actions serveur restent inactives en attendant une réponse du fournisseur de journalisation. Les gestionnaires de route s’arrêtent temporairement afin qu’un service de mesures puisse enregistrer un événement. Chacune de ces tâches en arrière-plan réduit progressivement le temps perçu de chargement, et l’effet cumulé est une application lente qui dissuade les utilisateurs.
L’API after() de Next.js sépare les effets secondaires du cycle de vie de la réponse. Vous placez le travail non essentiel dans after(), et le framework planifie son exécution une fois que la réponse a déjà été livrée. Le client reçoit son chargement immédiatement, tandis que les journaux, les analyses et autres tâches en arrière-plan s’exécutent par la suite, complètement en dehors du chemin critique.
Le reste de cet article explique le fonctionnement de after(), les scénarios où il est utile, ainsi que les compromis opérationnels à prendre en compte avant de l’utiliser en production.
Points clés
after()reporte les effets secondaires jusqu’à la fin de la réponse, évitant ainsi que le travail non essentiel ne perturbe le chemin de la requête destinée à l’utilisateur.
waitUntil(), qui est lié au Edge Runtime, after() se comporte de manière cohérente que vous exécutiez sur Node.js ou Edge, quel que soit le fournisseur d’hébergement.after() surviennent après que le client a déjà reçu une réponse, vous devez utiliser des blocs try-catch explicites pour les capturer et les enregistrer.after() ne soit terminée.Qu’est-ce que l’API after() et comment fonctionne-t-elle
after() prend un callback et le exécute une fois que le flux de réponse est fermé. La séquence est la suivante : l’environnement d’exécution met le callback en file d’attente, la réponse HTTP est envoyée au client, et ce n’est qu’à ce moment-là que le travail différé s’exécute. Le navigateur n’a jamais besoin d’attendre que ce callback soit terminé.
Dans une seule requête, Next.js conserve l’ordre dans lequel les appels à after() ont été enregistrés. Si votre gestionnaire appelle after() trois fois consécutivement, ces trois callbacks s’exécutent l’un après l’autre, dans cet même ordre, une fois que la réponse est prête. Cette garantie d’ordre est importante chaque fois qu’une tâche différée dépend d’une autre — par exemple, enregistrer une action dans un journal avant de invalider une entrée de cache qui reflète cette action.
C’est un modèle nettement différent de celui qui consiste simplement à lancer une promesse puis à l’oublier. Les promesses de type « fire-and-forget » ont tendance à absorber silencieusement les rejets non gérés, ce qui peut laisser vos journaux incomplets ou l’état de votre application désynchronisé. after() charge le moteur d’exécution de planifier explicitement ces tâches, ce qui rend beaucoup plus facile l’observation et la gestion adéquate des erreurs en production.
La différence entre l’exécution bloquante et non bloquante devient évidente une fois qu’on la mesure. Un gestionnaire qui enregistre des données de manière synchrone avant de répondre ajoute généralement de 50 à 150 millisecondes par requête. En déplaçant cette même opération d’enregistrement dans la fonction after(), le gestionnaire peut retourner en moins de 10 millisecondes — soit juste le temps nécessaire pour effectuer l’écriture dans la base de données. Du point de vue de l’utilisateur, la réponse semble instantanée, tandis que toutes les opérations de suivi s’effectuent invisiblement en arrière-plan.
Cas d’usage concrets : analyse des journaux et tâches en arrière-plan
Le suivi analytique en est l’exemple typique. Lorsqu’un client termine son achat, votre application enregistre l’achat et affiche une page de confirmation. La plateforme d’analyse n’a pas besoin de connaître cet achat au moment même où il a lieu — elle doit simplement en être informée ultérieurement. En reportant l’appel de suivi à after(), on peut réduire de 100 à 200 millisecondes le temps nécessaire pour finaliser l’achat sans perdre aucune donnée.
Le journalisation d’audit fonctionne de la même manière. Les règles de conformité exigent souvent que chaque modification d’état soit enregistrée quelque part, mais il n’y a aucune raison pour que l’utilisateur doive attendre que ce système d’audit confirme l’écriture. Le chemin critique gère la modification réelle de la base de données ; la fonction de rappel after() envoie l’entrée d’audit vers un système distinct, souvent un stockage de journaux optimisé pour les écritures ou une file d’attente de messages.
L’invalidation des caches est une autre solution appropriée, surtout lorsque l’invalidation concerne plusieurs services distants. Mettre à jour un contenu peut nécessiter le nettoyage des caches CDN, la suppression de clés Redis spécifiques et l’envoi de signaux aux clients WebSocket connectés. Rien de tout cela n’affecte ce que l’utilisateur voit dans la réponse. La mise à jour est enregistrée dans la base de données, une réponse indiquant le succès est renvoyée, et after() s’occupe ensuite de la cascade d’invalidation.
L’envoi d’e-mails ou de notifications s’intègre également bien dans after(), à condition que l’application n’ait pas besoin de signaler en temps réel un échec de livraison. Prenons l’exemple du réinitialisation de mot de passe : l’application enregistre le token de réinitialisation, renvoie un message de succès et envoie l’e-mail en arrière-plan. Si le fournisseur d’e-mails échoue, l’utilisateur peut simplement réessayer depuis l’interface sans voir d’erreur dans la demande initiale.
Cela permet d’éviter un mode de défaillance subtil mais coûteux. Si l’envoi d’e-mail a lieu en ligne de texte et que le fournisseur atteint son délai limite, l’utilisateur voit une erreur 500 bien que le jeton de réinitialisation ait été créé avec succès. Il tente à nouveau, ce qui génère un jeton dupliqué, et votre système doit alors nettoyer ces jetons orphelins sous peine de créer une faille de sécurité. Gérer l’envoi d’e-mail à l’intérieur de after() isole complètement ce type de défaillance — la réponse reste réussie, et une couche de surveillance distincte peut signaler indépendamment les problèmes de livraison.
La révalidation en arrière-plan et le réchauffement du cache relèvent du même principe. Supposons qu’un produit populaire soit épuisé : la mise à jour des stocks doit déclencher la révalidation des pages de la catégorie concernée ainsi que du cache de la page d’accueil. La mise à jour des stocks elle-même est renvoyée immédiatement, tandis que la fonction de rappel after() parcourt le graphe de dépendances et marque les entrées obsolètes à régénérer. Les visiteurs qui naviguent pendant cette fenêtre de révalidation pourraient voir des données légèrement dépassées, mais l’application reste rapide.
Mise en œuvre de after() dans les actions serveur et les gestionnaires de route
Les actions serveur peuvent appeler directement after() dans le cadre d’une mutation. L’action effectue son opération principale, planifie les effets secondaires nécessaires, puis restitue le contrôle au client. Next.js gère le cycle de vie en arrière-plan, s’assurant que la fonction de rappel s’exécute avant que la fonction sans serveur ne puisse être arrêtée.
Les gestionnaires de route suivent la même structure : le gestionnaire effectue le travail essentiel, envoie sa réponse, et déplace toute tâche secondaire dans after(). Cela fonctionne tant pour les gestionnaires de route d’App Router que pour les routes API de Pages Router configurées pour utiliser le runtime d’App Router.
Un détail important est que after() a accès au contexte complet qui existait au moment de sa registration. Tout ce qui a été capturé dans la fermeture — en-têtes de requête, données du corps analysées, état d’authentification — reste disponible à l’intérieur de la fonction de rappel sans aucune configuration supplémentaire.
Le middleware peut également s’appuyer sur after() pour enregistrer des métadonnées de requête sans ralentir davantage le gestionnaire dans la chaîne. Le middleware extrait les en-têtes nécessaires, planifie l’appel d’enregistrement, puis transmet la requête. L’entrée de journal est écrite de manière asynchrone tandis que la requête continue d’avancer vers sa destination.
Le degré de fiabilité réel de cette exécution dépend fortement du lieu d’hébergement. Vercel et des plateformes similaires prolongent la durée de vie de la fonction afin que les appels de retour after() aient le temps de s’achever. Si vous hébergez vous-même sur des conteneurs serverless, vous devez configurer soigneusement les délais de timeout : si le conteneur s’arrête avant que l’appel de retour ne soit terminé, ce travail est simplement perdu. C’est un vrai problème pour tout ce qui ne peut pas tolérer d’être interrompu, comme les événements de facturation ou l’enregistrement des données liées au respect des réglementations.
after() vs waitUntil() vs Approches traditionnelles
waitUntil(), issu du Edge Runtime, résout un problème similaire mais à un niveau inférieur : il maintient une fonction active jusqu’à ce qu’une promesse donnée soit résolue, empêchant ainsi le runtime de fermer les processus prématurément. after() crée une abstraction par-dessus ce mécanisme et se comporte de la même manière que ce soit sous Node.js ou sur Edge.
Les projets qui utilisent déjà waitUntil() dans un contexte Edge Runtime peuvent adopter progressivement after(). Les deux ne sont pas identiques en termes de structure : waitUntil() prend directement une promesse, tandis que after() enrobe une fonction de rappel. Tous deux atteignent le même objectif, à savoir éviter une terminaison prématurée, mais after() est plus facile à utiliser lorsque l’on doit enchaîner plusieurs tâches en arrière-plan, car il permet d’éviter de gérer manuellement plusieurs promesses.
Les techniques « lance et oublie » basées sur des promesses non attendues ou des appels à setTimeout ne fournissent aucune garantie réelle. Le moteur d’exécution peut s’arrêter avant que la promesse ne soit terminée, éliminant discrètement tout travail en cours. Les bibliothèques de journalisation conçues autour de process.nextTick() ou setImmediate() se heurtent au même problème une fois déployées dans des environnements serverless. En revanche, after() exprime clairement l’intention et donne à la plateforme une véritable chance de la respecter.
Les files d’attente de messages restent l’option la plus fiable pour le traitement en arrière-plan, mais elles entraînent des coûts opérationnels réels. Il faut une infrastructure, des travailleurs dédiés, des mécanismes de tentative répétée et des tableaux de bord de surveillance pour que tout fonctionne correctement. Pour des effets secondaires mineurs tels que l’écriture de journaux ou la invalidation d’une entrée dans le cache, un tel dispositif est excessif. after() se situe entre ces deux extrêmes : il est plus solide qu’une simple appel sans suivi, mais bien moins complexe que la mise en place d’un système basé sur des files d’attente.
Cette compromis se manifeste de la manière la plus évidente dans la façon dont les pannes sont gérées. Une file d’attente de messages réessaie automatiquement les tâches échouées et peut diriger les pannes persistantes vers une file de lettres mortes afin d’en examiner le contenu ultérieurement. En revanche, un appel de retour after() ne s’exécute qu’une seule fois par requête. S’il échoue, ce travail est perdu à moins d’avoir mis en place son propre mécanisme de réessai. C’est un risque acceptable pour des opérations peu critiques, mais pour tout ce qui est essentiel, comme le traitement d’un paiement ou la mise à jour des stocks, une file d’attente appropriée reste l’outil idéal.
Considérations de production : gestion des erreurs et garanties d’exécution
Gérer les erreurs à l’intérieur de after() relève entièrement de vous : enveloppez la logique dans des blocs try-catch explicites. Une exception levée à l’intérieur du callback n’atteindra pas le client, car la réponse a déjà été envoyée au moment où le callback s’exécute. La plateforme enregistrera l’échec, mais récupérer la situation — par des tentatives répétées, des alertes ou une logique de secours — relève de la responsabilité de l’application.
Le degré de fiabilité avec lequel la fonction de rappel s’exécute dépend fortement du lieu d’hébergement de l’application. Sur Vercel, l’exécution des fonctions est prolongée afin de permettre aux fonctions after() de s’exécuter, jusqu’à l’expiration du délai configuré. L’exécution de Next.js sur AWS Lambda exige une ajustement minutieux des délais d’expiration pour éviter que la fonction ne soit réutilisée avant que le rappel ne soit terminé. GCP Cloud Run et Azure Container Instances présentent des contraintes similaires. Dans tous ces cas, le schéma d’échec récurrent est identique : la fonction expire avant que le travail différé ne soit achevé, et ce travail est définitivement perdu.
L’observabilité devient essentielle une fois que ce schéma est mis en production. Les journaux d’application réguliers couvrent le cycle de vie principal de la requête, mais les appels de retour after() s’exécutent après que ce cycle de vie a techniquement pris fin, en dehors du contexte habituel des journaux. Les outils de traçage distribué tels que OpenTelemetry doivent être configurés pour capturer explicitement cet appel de retour en tant que span distinct. Si cette étape est omise, tous les erreurs survenant à l’intérieur de after() deviennent en fait invisibles pour ceux qui surveillent le système.
Les tests de charge révèlent une autre dimension du comportement de ce schéma. Prenons un gestionnaire de route qui exécute une tâche prenant 500 ms dans la méthode after(). Testé isolément, ce chemin semble rapide. Cependant, avec 100 requêtes simultanées, la plateforme doit exécuter 100 appels en retour presque en même temps, ce qui peut la rendre incapable de suivre. Le chemin principal des requêtes reste réactif, mais les tâches différées commencent à s’accumuler. L’infrastructure doit être dimensionnée non seulement en fonction du volume des requêtes entrantes, mais aussi en fonction de la charge supplémentaire générée par les tâches en arrière-plan.
Cette approche entre également en conflit avec les API qui imposent des limites de fréquence. Imaginez 1 000 requêtes qui atteignent votre application en une minute, chacune planifiant une appel à un service d’analyse à l’intérieur de after(). Une fois la vague de réponses stabilisée, le fournisseur d’analyse reçoit soudainement environ 1 000 appels consécutifs, ce qui peut le pousser à limiter ou à rejeter ces appels. Le regroupement de la logique de rappel aide dans ce cas : au lieu d’envoyer une requête pour chaque événement, on accumule les événements en mémoire et on les envoie ensemble par groupes plus importants et moins fréquents.
Cependant, le regroupement des opérations soulève ses propres problèmes de mise en configuration. Le buffer qui contient les événements accumulés reste en mémoire jusqu’au déclenchement du nettoyage, consommant des ressources en permanence. Une augmentation soudaine du trafic peut épuiser cette mémoire avant même que le nettoyage planifié ne s’active. L’autre option consiste à utiliser la fonction de rappel after() pour ajouter les événements dans une file d’attente persistante au lieu de les stocker en mémoire, mais cela revient à réintroduire une grande partie de la complexité que after() était censé vous aider à éviter.
Finalement, le fait que after() soit la bonne solution dépend du niveau de dommage causé par la perte des tâches différées. Des éléments tels que les événements d’analyse, les enregistrements de traçabilité et l’invalidation du cache peuvent supporter une exécution occasionnellement manquée sans compromettre des fonctionnalités essentielles. En revanche, les confirmations de paiement, les modifications d’inventaire et les événements liés à la sécurité ne le peuvent pas. Pour ce type de tâches, une file d’attente de messages ou un traitement synchrone simple reste la solution la plus sûre, même si cela entraîne une légère perte de temps de réponse.
Questions fréquentes
Les appels de retour après() peuvent-ils accéder à des données liées à la requête, comme les en-têtes ou les cookies ?
Oui. La fonction de rappel conserve le contexte existant au moment où after() a été appelée, de sorte que toutes les variables, valeurs d’en-tête ou données de cookies disponibles à ce moment restent accessibles à l’intérieur de la fonction de rappel. En pratique, cela signifie que vous pouvez faire référence à des éléments tels que l’utilisateur authentifié, le corps de la requête analysé ou les métadonnées extraites sans avoir besoin de les transmettre par un canal séparé.
Que se passe-t-il si une fonction de rappel after() génère une erreur non gérée ?
Le moteur d’exécution enregistre l’erreur, mais celle-ci n’atteint jamais le client, car la réponse a déjà été envoyée au moment où la fonction de rappel s’exécute. Il revient à l’application d’envelopper les fonctions de rappel after() dans des blocs try-catch et d’envoyer les échecs à un outil de surveillance ou à un mécanisme de tentative répétée. Sans cela, les échecs disparaissent simplement sans laisser de trace.
after() fonctionne-t-il aussi bien dans les environnements Node.js que Edge Runtime ?
Oui, l’API compense les différences entre les deux environnements d’exécution. Avec Edge Runtime, elle s’appuie sur la même sémantique que waitUntil(). Avec Node.js, elle utilise le mécanisme spécifique à la plateforme disponible pour prolonger la durée de vie de la fonction. L’interface reste identique dans les deux cas, mais le niveau de garantie réelle de l’exécution dépend toujours de la configuration du fournisseur d’hébergement.
En quoi after() diffère-t-il de l’utilisation d’une file d’attente de messages pour les tâches en arrière-plan ?
Une file d’attente de messages offre des tentatives automatiques, un traitement des messages non livrés et une exécution fiable, au prix d’une charge opérationnelle supplémentaire. after() est une solution beaucoup plus légère, adaptée aux effets secondaires tels que l’enregistrement ou l’invalidation du cache, où une perte occasionnelle n’est pas un problème majeur. Lorsque le travail ne doit absolument pas être perdu, une file d’attente reste la meilleure solution.
Plusieurs appels à after() peuvent-ils s’exécuter en même temps ou se déroulent-ils séquentiellement ?
Dans une seule requête, les appels de fonction s’exécutent l’un après l’autre, dans l’ordre où ils ont été enregistrés. Si vous appelez after() trois fois, le moteur d’exécution termine complètement le premier appel avant de commencer le deuxième, puis le troisième. Cet ordre est important lorsque l’une des opérations différées dépend d’une autre, par exemple pour enregistrer une action avant de invalider une entrée de cache correspondante.
Conclusion : quand utiliser after() dans votre application Next.js
after() vous permet d’extraire les tâches non essentielles du chemin de requête attendu par l’utilisateur, sans avoir à mettre en place d’infrastructure de file d’attente. Des tâches telles que le suivi analytique, l’enregistrement des audits, l’invalidation du cache et l’envoi de notifications conviennent parfaitement, car l’utilisateur reçoit sa réponse immédiatement tandis que l’application s’occupe discrètement du reste en arrière-plan.
Le problème, c’est que l’exécution n’est pas garantie. Les plateformes serverless détruisent rapidement les fonctions, et si l’environnement interrompt le callback au milieu de son exécution, ces tâches sont perdues. Par conséquent, les délais de timeout doivent être configurés en fonction des besoins du callback, et chaque callback after() doit inclure sa propre gestion des erreurs. Lorsque la perte occasionnelle de ces tâches est acceptable, la simplicité de after() en vaut la peine. Sinon, une file d’attente de messages reste l’option plus fiable.
Comprendre ces schémas devrait suffire pour commencer à utiliser after() efficacement dans une application Next.js. Appliqué avec réflexion, il peut avoir un impact significatif sur la vitesse perçue par les utilisateurs, et cette différence est particulièrement importante sur les routes à fort trafic, où de petits retards s’accumulent et peuvent pousser les utilisateurs à abandonner complètement leur session.
Lectures associées
- Benchmarking du compilateur Go de TypeScript 7 sur une application Next.js réelle — Une comparaison pratique des temps d’exécution de tsc entre TypeScript 6 et 7 sur un codebase Next.js réel, incluant une erreur de non-conformité dans les tests CI et des conseils pour l’upgrade.