Next.js App Router en production : ce qui change vraiment
Les tutoriels sur l’App Router s’arrêtent tous au même endroit : la page s’affiche en développement. Voici ce qui se passe après, sur une application Next 15 qui tourne pour de vrai, avec une base, des sessions et des données qui changent.
params est asynchrone, et ça se propage
Depuis Next 15, params et searchParams sont des promesses.
export default async function Page(props: { params: Promise<{ id: string }> }) {
const { id } = await props.params
}
La conséquence n’est pas l’await, elle est que le composant doit être
async. Et un composant asynchrone ne peut pas être appelé par un composant
client. Sur une application existante, la migration remonte donc l’arbre jusqu’à
trouver une frontière, et cette remontée est le vrai coût de la mise à jour.
Deux issues quand la remontée devient trop longue : déplacer le sous-arbre côté client entièrement, ou passer la donnée en propriété depuis le parent déjà asynchrone. La deuxième est presque toujours la bonne, et elle rend le composant plus simple au passage.
La mise en cache par défaut
Next met en cache agressivement, et il le fait bien. Le problème est qu’une page d’administration qui affiche l’état d’aujourd’hui n’a aucune raison d’être mise en cache, et pourtant elle l’est.
Sur une application dont presque toutes les pages lisent une base, la ligne qui règle le sujet est :
export const dynamic = 'force-dynamic'
Posée page par page, pas globalement, sinon on perd le bénéfice sur les pages qui peuvent vraiment être mises en cache. Le symptôme qui doit vous alerter : une donnée modifiée dans l’interface qui réapparaît à l’ancienne valeur après un rechargement, et qui se corrige toute seule au bout de quelques minutes.
output: 'standalone' n’embarque pas tout
Pour un déploiement en conteneur, standalone produit un dossier autonome avec
un serveur minimal. Il ne contient que ce que le graphe de modules atteint.
Ce qui s’en trouve exclu, et qu’il faut recopier à la main dans l’image :
- les binaires que vous appelez en sous-processus,
- le client de base de données généré, si le générateur tourne après la construction,
- tout fichier lu par chemin plutôt qu’importé.
Le symptôme est toujours le même : ça marche en local, ça échoue au démarrage du
conteneur avec un module introuvable. Le remède est de lire ce que contient
réellement .next/standalone avant de construire l’image, pas après.
Un cas qui m’est arrivé et qui m’a coûté une heure : un script d’administration
importait une bibliothèque absorbée dans le paquet de l’application. Elle n’était
donc plus dans node_modules, et le script échouait alors que l’application
tournait.
Les variables d’environnement, figées à la construction
NEXT_PUBLIC_* est inséré dans le paquet au moment de la construction. Une image
construite avec l’adresse d’une instance ne peut pas servir une autre instance.
Si vous livrez la même image à plusieurs clients, tout ce qui varie doit être lu côté serveur à l’exécution, et descendu au client par propriété. Ça fait plus de code. Sans ça, une image ne sert qu’un client.
Les composants serveur et la session
Un composant serveur lit les cookies et la session sans passer par une requête réseau, ce qui est confortable. Mais il s’exécute avant toute interaction, donc il ne peut pas réagir à un changement de droit survenu entre-temps.
La règle que j’applique : la vérification de droit qui compte est celle de la route d’API, pas celle du composant. Le composant cache un bouton, la route refuse l’action. Cacher sans refuser donne une interface propre et une application ouverte.
L’hydratation échoue en silence
Un écart entre le rendu serveur et le rendu client produit une erreur numérotée dans la console, souvent la 418 ou la 423, et rien d’autre. La page s’affiche, puis les interactions ne répondent pas.
Les trois causes que je rencontre, dans l’ordre :
- une date formatée sans fuseau fixe, qui diffère entre le serveur et le navigateur,
Math.randomouDate.nowdans un rendu,- une lecture de
localStoragefaite pendant le rendu plutôt que dans un effet.
La première est de loin la plus fréquente, et la plus sournoise parce qu’elle ne se manifeste qu’à certaines heures.
Le temps de construction, toujours pas réglé
Le temps de construction. Sur l’application citée, une construction complète prend quatre à six minutes, et le cache d’une construction à l’autre reste capricieuse en conteneur. Je n’ai pas trouvé de réglage satisfaisant, et j’ai arrêté d’essayer. Les contournements que j’avais empilés coûtaient plus d’entretien que les minutes qu’ils faisaient gagner sur la durée.