Si tu viens pour comprendre pourquoi router.refresh() peut faire exploser tes requêtes en Next.js 16, tu es au bon endroit. Le piège est vicieux parce qu’il ressemble à une « petite ligne de sécurité » qu’on met un peu partout pour être sûr d’avoir des données fraîches. Et en prod, ça peut se transformer en tsunami de fetch, en cache qui chauffe et, dans certains cas, en facture qui grimpe (coucou les écritures ISR).
Le point clé: en v16, après un router.refresh(), Next peut re-préfetch agressivement tous les <Link> visibles dans le viewport. En v15, c’était beaucoup plus « lazy ». Si ta page a une liste de liens (dashboard, table, listing, menu dense), tu viens de créer une charge réseau et serveur… juste en « rafraîchissant ».
Ce qui a changé entre Next.js 15 et 16 : refresh = re-prefetch des liens visibles
Dans l’App Router, Next préfetch des routes via <Link> quand elles entrent dans le viewport. C’est une optimisation UX, et sur le papier c’est très bien: tu survoles, tu cliques, ça part vite.
Sauf qu’en Next.js 16, il y a une régression (ou un changement de comportement selon comment tu le vis): après router.refresh(), Next peut relancer immédiatement le prefetch de tous les <Link> visibles. Et le « coût » n’est pas un seul appel. En App Router, une navigation peut déclencher plusieurs requêtes RSC (par segments), et si derrière tu touches à de l’ISR, tu peux aussi déclencher des écritures côté plateforme.
Le résultat est contre-intuitif: tu n’as pas ajouté un « polling », tu n’as pas ajouté une boucle. Tu as juste mis un refresh « au bon endroit ». Et pourtant, tu peux te retrouver avec un pattern du style « refresh → re-prefetch en masse → refresh ailleurs → re-prefetch en masse », surtout si ton app a des interactions fréquentes (filtres, modals qui confirment une action, switch d’onglets, etc.).
Sponsorisé par Le Scribouillard
Besoin de contenu optimisé SEO ?
Utilisez la meilleure plateforme française de création de contenu assistée par IA ! Et générez des articles pour moins de 1€ !
Les symptômes en prod : pics de requêtes, ISR writes qui explosent, latence qui monte
Sur le terrain, tu le vois rarement tout de suite dans ton navigateur. En local, ça passe, et tu te dis « bon, ça refetch, normal ». En prod, avec du trafic, ça se transforme en bruit de fond énorme.
Le premier signal, c’est un pic de requêtes serveur juste après des actions utilisateur « anodines ». Typiquement, un bouton « Enregistrer », un toggle, un changement de filtre, une action qui appelle une Server Action puis un router.refresh() pour resynchroniser l’UI. Tu crois rafraîchir la page. En fait, tu viens de dire à Next: « ok, et maintenant précharge tout ce que l’utilisateur voit comme liens, au cas où ».
Le deuxième signal, plus douloureux, c’est le côté plateforme: si tu as des pages en ISR, ou une stratégie de cache/revalidate qui implique des régénérations, tu peux voir les métriques d’ISR Writes partir en fusée. Et ça, c’est à la fois du coût et du risque: plus d’écritures signifie souvent plus de contention, plus de latence, et des comportements « bizarres » quand tu arrives sur des limites.
Le troisième signal, c’est l’UX qui se dégrade sans raison évidente: la machine fait plein de choses en arrière-plan. Même si le user ne clique pas les liens, ton app consomme de la bande passante et du CPU (client + serveur). Sur mobile ou réseau pourri, ça se ressent vite.
Comment le prouver proprement (sans débattre “au feeling”)
La difficulté, c’est que « il y a des requêtes » n’est pas une preuve. Ce que tu veux prouver, c’est le lien causal entre router.refresh() et le re-prefetch massif des liens visibles.
Le test le plus rentable, c’est de prendre une page qui affiche une liste (20, 50, 200 items, peu importe), où chaque item a un <Link> vers une page détail. Tu ajoutes un bouton qui fait uniquement router.refresh() (sans mutation). Puis tu observes le réseau après le clic. Si tu vois partir une rafale de requêtes correspondant aux destinations des liens visibles, tu as déjà un signal très fort.
Ensuite, tu confirmes côté serveur. En prod, le bon move est de corréler une action utilisateur (ou un endpoint de refresh) avec un pic de hits sur les routes préfetchées. Si tu as de l’observabilité (logs structurés, request IDs, tracing), tu peux carrément isoler un intervalle de quelques secondes et lister les routes touchées: tu vas retrouver les routes des liens « dans le viewport » au moment du refresh.
Si tu dois faire ça vite, sans stack de tracing, ajoute au moins un log minimal sur les routes suspectes (ou sur le handler de la page App Router) et regarde la densité d’appels au moment du refresh. Ce n’est pas « beau », mais c’est souvent suffisant pour arrêter le débat.
Une mini-repro qui met le problème sous ton nez
Je te laisse un exemple volontairement simple. L’idée est de produire un comportement binaire: sans refresh, pas de rafale; avec refresh, rafale. Tu mets ça dans une page de listing.
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
export default function Listing() {
const router = useRouter();
const items = Array.from({ length: 100 }, (_, i) => ({ id: i + 1 }));
return (
<main>
<button onClick={() => router.refresh()}>
Refresh
</button>
<div style={{ marginTop: 24 }}>
{items.map((it) => (
<div key={it.id} style={{ padding: 8 }}>
<Link href={`/items/${it.id}`}>Item {it.id}</Link>
</div>
))}
</div>
</main>
);
}En Next.js 16, si ton viewport affiche disons 15 liens, tu peux voir partir des requêtes de préfetch vers ces 15 destinations (et parfois davantage selon le rendu, les segments, et comment Next découpe les fetch RSC). Sur un dashboard réel, avec des listes, des blocs, des menus, tu arrives vite à des chiffres ridicules.
Pourquoi ça peut déclencher un “write storm” ISR (et pas juste du trafic)
Le mot important ici, c’est « préfetch ». Beaucoup de devs associent préfetch à « cache client », donc « pas grave ». Dans Next App Router, le préfetch peut quand même taper ton infra. Parce que pour précharger, il faut bien récupérer du RSC, donc exécuter du code serveur, donc toucher des caches, parfois des backends, parfois déclencher des mécanismes de régénération ou de revalidation.
Et là, tu as un multiplicateur discret: une page avec 50 liens visibles, c’est potentiellement 50 préfetch. Si chacun coûte plusieurs requêtes (segments), tu multiplies encore. Si derrière, tes routes sont en ISR ou touchent un cache qui déclenche des écritures, tu ne fais pas que lire, tu écris. Et quand tu écris, tu paies souvent plus cher, tu atteins des quotas plus vite, et tu te crées des incidents qui n’ont rien à voir avec « la feature » initiale.
Le pire, c’est quand tu as mis router.refresh() comme « réflexe de cohérence » après une mutation. Par exemple: tu fais une action, tu refresh pour mettre à jour un compteur, et tu te retrouves à précharger 40 pages détails que l’utilisateur n’ouvrira jamais. Tu as gagné une UI « toujours fraîche », mais tu as aussi gagné une dette de perf et de coût.
Mitigations réalistes : comment éviter de transformer refresh en DDoS interne
Je vais être franc: la meilleure mitigation, c’est de réduire l’usage de router.refresh(). Pas « jamais », mais « pas par défaut ». Je vois trop de code où refresh devient un pansement universel, parce que c’est simple et que ça masque des problèmes de synchronisation d’état. En v16, ce pansement a un effet secondaire beaucoup plus sale.
Quand tu fais une mutation, ton premier réflexe devrait être de mettre à jour ce qui est strictement nécessaire. Souvent, tu peux faire une mise à jour optimiste côté client, ou renvoyer la donnée utile depuis la Server Action et la réinjecter dans le composant, plutôt que de dire à toute la page « repars de zéro ». Oui, ça demande un peu plus de discipline. Mais en échange, tu ne paies pas le refresh global.
Deuxième levier très concret: maîtriser le prefetch sur les zones à risque. Une liste de 200 lignes avec des liens vers des pages détails n’a pas besoin d’être préfetchée « en masse » après un refresh. Sur ces liens-là, désactiver le prefetch est souvent un compromis sain: tu perds un peu de snappiness au clic, mais tu récupères énormément en stabilité et en coût.
import Link from "next/link";
export function RowLink({ id }: { id: number }) {
return (
<Link href={`/items/${id}`} prefetch={false}>
Ouvrir
</Link>
);
}Ce que j’aime bien en pratique, c’est appliquer ça de manière ciblée: tu gardes le prefetch sur les liens « premium » (navigation principale, parcours court, pages très consultées), et tu le coupes sur les gros listings, les pages outils internes, les tables d’admin, tout ce qui a beaucoup de liens dans le viewport.
Troisième levier: réduire le nombre de liens “in viewport” au moment où tu refresh. C’est bête, mais très efficace. Si tu as un mega-dashboard où tout est visible en même temps, tu invites Next à préfetch plein de trucs. Parfois, juste paginer, virtualiser une table, ou déplacer une liste sous un accordéon fermé par défaut suffit à enlever 80% du problème. Ce n’est pas qu’un sujet UI, c’est un sujet réseau/serveur.
Quatrième levier, plus « architecture App Router »: découper proprement tes segments pour éviter qu’une action locale déclenche une invalidation globale. Si ton refresh est là parce que « sinon je ne sais pas comment mettre à jour ce composant serveur », c’est souvent le signe que tu peux isoler un bout de page, ou revoir la frontière client/server. Je sais, c’est le genre de phrase qui sonne “yaka”. Mais en Next, c’est souvent là que se joue la différence entre une app stable et une app qui surchauffe.
Erreurs fréquentes que je vois avec router.refresh() (et qui deviennent toxiques en v16)
La première, c’est le refresh “réflexe” après chaque action. Tu cliques « archiver », refresh. Tu changes un filtre, refresh. Tu fermes un modal, refresh. Tu ne sais plus ce qui rafraîchit quoi, mais « au moins c’est cohérent ». En Next.js 16, tu crées surtout une machine à préfetch.
La deuxième, c’est de ne pas distinguer un besoin de revalidate d’un besoin de rerender. Parfois tu veux juste invalider une ressource (un tag, un chemin) pour que la prochaine requête serve la bonne version. Et parfois tu veux juste mettre à jour l’UI locale. router.refresh() fait un gros marteau. Si ton problème est un clou minuscule, tu casses la table.
La troisième, c’est de découvrir le problème uniquement via la facture ou un quota. Là, il est trop tard: tu as déjà mis du refresh partout, tu as des users, et tu dois patcher dans l’urgence. Si tu upgrades en v16, teste ces scénarios avant. Pas « est-ce que ça compile », mais « est-ce que ça déclenche 50 requêtes quand je clique sur ce bouton ».
Avant d’upgrader en Next.js 16 : ce que je vérifierais côté trafic et coût
Je ne te propose pas une checklist qui fait plaisir au CI. Je parle d’un check « prod ». Repère les pages qui ont beaucoup de <Link> visibles: listings, dashboards, pages d’admin, recherches, tables. Ensuite, repère où tu utilises router.refresh(). Si tu as un refresh dans un composant client qui vit sur une page avec des listes, tu as déjà un candidat.
Ensuite, reproduis les actions utilisateurs « normales » (pas des tests artificiels): enregistrer un formulaire, toggle un statut, changer un filtre, valider un paiement, etc. Ouvre l’onglet réseau, mais surtout corrèle avec des logs côté serveur. Ce que tu cherches, c’est un pattern stable: « action → refresh → rafale de préfetch ». Si tu le vois, tu sais quoi faire: réduire les refresh, couper le prefetch là où il est toxique, et isoler les segments.
Enfin, surveille des métriques qui parlent argent et capacité: requêtes par pageview, taux de cache hit/miss, écritures ISR, p95 de latence sur les routes concernées. Si tu n’as pas ces métriques aujourd’hui, c’est justement le moment de les poser. Le bug le plus cher, c’est celui qui a l’air « normal » pendant deux semaines.
Mon avis : router.refresh() n’est pas “gratuit”, et Next 16 le rappelle violemment
J’aime Next.js, mais il y a un truc à accepter: l’App Router est puissant, et donc ses effets de bord peuvent être puissants aussi. router.refresh() est une API super pratique, mais en v16 elle peut déclencher un comportement de préfetch qui n’a rien d’anodin. Si tu l’utilises comme un flush global « pour être safe », tu es en train de créer une dette invisible.
Le bon arbitrage, ce n’est pas de « désactiver le prefetch partout » ou de « ne jamais refresh ». C’est de choisir où tu veux de la magie, et où tu veux du contrôle. Sur une nav principale, le prefetch est souvent une vraie valeur. Sur un tableau de 200 liens, c’est juste du bruit. Et sur une mutation, un refresh global est rarement le meilleur contrat.
Conclusion : traite ça comme un sujet perf + coût, pas comme un détail de routing
Si tu as vu passer des pics de requêtes après une upgrade, ou des ISR writes qui montent sans explication, ne pars pas immédiatement sur « on a un bot » ou « c’est le CDN ». Va regarder tes router.refresh() et ce qu’il se passe autour des <Link> visibles. La cause peut être bêtement locale, reproductible, et donc corrigeable.
Et si tu es en train de planifier une migration Next 15 → 16, fais-toi un cadeau: teste tes pages à gros volume de liens avec des actions qui refresh. C’est typiquement le genre de problème qui ne se voit pas au build, mais qui se paye cash en prod.