Tu fais un upgrade Next.js « normal ». Rien de fancy. Et derrière, deux trucs quasi invisibles te bouffent une demi-journée : les liens vers des ancres #id se mettent à tomber au mauvais endroit (pile sous ton header sticky, donc tu ne vois pas le titre), et ton environnement de dev devient inutilisable depuis ton téléphone sur le Wi‑Fi (menus morts, navigation cassée, console qui hurle WebSocket).
Ce que j’aime moyennement avec ce genre de régression, c’est que tu peux passer du temps à douter de ton code. Alors que souvent, la bonne approche c’est « reproduire froidement », isoler, et poser un fix robuste. Je te montre comment je m’y prends, avec des solutions propres. Pas du scotch.
Next.js 16.2 : reconnaître vite les deux symptômes (sans te raconter d’histoires)
Le premier symptôme est assez sournois : tu cliques un lien du type /docs#installation, l’URL se met bien à jour, le scroll se fait… mais l’ancre arrive trop bas ou trop haut. Dans le cas le plus classique, ton sticky header recouvre le titre ciblé. Donc l’utilisateur a l’impression que le lien est « faux », alors que techniquement tu es au bon endroit.
Le second est encore plus vicieux parce qu’il ressemble à un bug front aléatoire : en dev, quand tu ouvres le site via l’IP privée du laptop (ex. http://192.168.1.50:3000) sur un téléphone connecté au même réseau, la page HTML peut s’afficher, mais les interactions JS partent en vrille. Souvent tu vois des erreurs WebSocket liées au HMR, ou des assets /_next/… qui ne se chargent pas comme prévu. Résultat : tu crois que ton code est cassé « seulement sur mobile ». En réalité, c’est ton dev server et ses règles d’origin qui te mettent dehors.
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€ !
Ancres #id + header sticky : pourquoi ça casse (et pourquoi ça se voit seulement après upgrade)
Le scroll vers une ancre, c’est un mélange un peu fragile de comportements navigateur et de comportements framework. Le navigateur sait scroller vers l’élément dont l’ID matche le hash. Mais dès que tu ajoutes un header sticky, tu introduis un décalage visuel qui n’existe pas dans le modèle mental du navigateur. Pendant un moment, tu peux avoir l’impression que « ça marche quand même » parce que certains enchaînements de navigation ou certains timings (chargement, hydrate, fonts) tombent bien.
Quand tu upgrades Next.js, tu changes parfois des choses autour du timing : quand le DOM est considéré prêt, quand la page est hydratée, comment les transitions de route déclenchent le scroll, comment le focus est géré. Et sur un cas limite (header sticky + sections dynamiques + ancres), le moindre changement peut faire apparaître un bug qui était déjà latent.
Mon conseil : ne pars pas en chasse d’un « bug d’ancre » abstrait. Pose un cas reproductible : une page longue, un header sticky, et un lien qui pointe vers une section en dessous de la ligne de flottaison. Si tu peux faire varier uniquement la version Next.js et observer la différence, tu as déjà gagné la moitié du combat.
Le fix le plus propre (et le plus stable) : scroll-margin-top
Si ton problème est « l’ancre arrive sous le header sticky », tu n’as pas envie de bricoler du JS qui fait des window.scrollTo avec des setTimeout. Ça marche… jusqu’au prochain changement de layout, jusqu’au prochain device, jusqu’à la prochaine font qui met 200ms à se charger. Et tu vas le repayer.
La solution CSS moderne, faite pour ça, c’est scroll-margin-top. Tu l’appliques à l’élément qui porte l’ID (souvent ton h2 ou un wrapper au-dessus), et tu lui dis « quand tu me scroll vers moi, garde une marge en haut ».
/* Exemple : header sticky de 72px + un peu d’air */
:root {
--sticky-header: 72px;
}
[id] {
scroll-margin-top: calc(var(--sticky-header) + 12px);
}
/* Option plus ciblée si tu ne veux pas impacter tous les IDs */
.prose h2[id],
.prose h3[id] {
scroll-margin-top: calc(var(--sticky-header) + 12px);
}
Ce fix a deux gros avantages : il ne dépend pas du framework, et il tient même si tu changes de router, de composants, ou de stratégie de rendu. C’est franchement le premier truc que je mets en place dès qu’il y a des ancres et un header sticky.
Quand scroll-margin-top ne suffit pas : focus, contenu asynchrone et « faux #id »
Il y a des cas où tu vas régler 80% du problème, mais il reste des situations bizarres. Typiquement : une section qui se déplace après coup parce que des images se chargent, parce que ton contenu est conditionnel, parce que tu affiches un accordéon qui s’ouvre, ou parce qu’un bloc au-dessus change de hauteur après hydration. Là, l’ancre a « scrollé au bon endroit à l’instant T », puis le layout a bougé. Et tu te retrouves décalé.
Dans ces cas-là, je regarde trois choses. D’abord, est-ce que l’ID est porté par le bon élément. Si tu mets l’ID sur un span inline dans un titre, le scroll target peut être moins stable qu’un bloc. Ensuite, est-ce que le header sticky a une hauteur stable sur toutes les largeurs (le classique : une ligne de navigation en plus sur mobile, et tes offsets ne matchent plus). Enfin, est-ce qu’un script (ou le router) force un scroll « de confort » après navigation et vient écraser le scroll vers le hash.
Si tu as un contenu vraiment dynamique, le fix « propre » n’est pas forcément de surcharger le navigateur. C’est souvent de rendre la page moins mouvante : réserver l’espace des médias, éviter les blocs qui apparaissent tard, stabiliser les hauteurs critiques. Le scroll vers ancre déteste les layouts qui dansent.
Dev sur IP privée (LAN) après upgrade : ce qui casse vraiment, c’est le WebSocket du HMR
Le scénario classique : tu lances next dev, tout est ok sur http://localhost:3000. Tu veux juste tester depuis un téléphone, donc tu tapes l’IP LAN du laptop. Et là, tu te retrouves avec une app « qui a l’air de charger », mais où le JS se comporte comme si la moitié du runtime était absent.
Dans la pratique, beaucoup de symptômes viennent du fait que le dev server s’appuie sur des connexions (dont WebSocket) pour le HMR, l’overlay d’erreurs, parfois des bouts de runtime en dev, et que ces connexions deviennent plus strictes quand il y a un check d’origin/host. Une mise à jour peut durcir ces règles (ce qui n’est pas absurde côté sécurité), mais toi tu le vis comme un bug « réseau » apparu sans prévenir.
Premier réflexe : ne te focalise pas sur « mon menu ne s’ouvre plus ». Ouvre les DevTools sur l’appareil (ou via remote debugging), et cherche ce qui ne se charge pas : des erreurs CORS, des requêtes /_next en échec, et surtout un WebSocket qui refuse de se connecter ou qui est bloqué.
Debug rapide : prouver que c’est un problème d’origin/WS (et pas ton code)
Quand je veux trancher vite, je fais simple. Je compare le comportement entre localhost et l’IP LAN en gardant exactement le même build de dev. Si le bug n’existe que via IP, c’est rarement « ton composant React ». C’est quasiment toujours un problème de host/origin, de firewall, ou de proxy réseau.
Ensuite je regarde l’onglet Network et je filtre sur « WS ». Sur Next.js, tu vas souvent voir des endpoints liés au hot reload. Si le WebSocket échoue, tu as un point d’entrée concret. Et même si tu t’en fous du HMR, en dev tu n’as pas envie de vivre sur une app qui spamme des erreurs et qui charge des morceaux de runtime de travers.
Dernier truc très terre à terre : vérifie que tu écoutes bien sur toutes les interfaces. Beaucoup de gens pensent tester « sur le Wi‑Fi » alors qu’ils écoutent uniquement sur localhost, et qu’ils ont juste eu un effet de cache ou une URL qui pointe ailleurs. En général, si tu peux charger l’HTML via IP, c’est que l’écoute est OK, mais ça vaut le coup de le confirmer.
Fixes qui marchent en vrai : écouter sur 0.0.0.0, et autoriser les bonnes origins
La base, c’est de lancer le serveur de dev sur une interface accessible depuis le réseau local, donc typiquement 0.0.0.0. Ensuite, sur les versions où Next.js vérifie plus strictement les origins en dev, tu dois parfois autoriser explicitement l’origin que tu utilises (ton IP LAN, ou un hostname local).
La config exacte peut évoluer selon les versions, mais l’idée reste la même : dire à Next « en dev, cette origin est légitime ». Voilà un exemple de config qui illustre ce principe quand l’option est disponible dans ton setup.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
// Autorise l'accès au dev server via IP/hostname LAN (à adapter)
allowedDevOrigins: [
'http://192.168.1.50:3000',
'http://mon-laptop.local:3000'
]
}
}
module.exports = nextConfig
Deux remarques importantes. D’abord, n’ouvre pas ça n’importe comment : l’objectif est de faire fonctionner ton téléphone et éventuellement un autre poste de dev, pas d’exposer ton serveur de dev à tout un réseau d’entreprise. Ensuite, si tu peux, je préfère souvent utiliser un hostname local stable (mDNS type *.local ou une entrée dans /etc/hosts) plutôt que de dépendre d’une IP qui change. Le jour où ton DHCP te bouge l’adresse, tu vas te re-casser les dents dessus.
Si tu veux un dev LAN « zéro surprise » : passe par un proxy local
Quand tu as un environnement un peu corsé (VPN, WAF local, proxy corporate, device iOS capricieux), le plus robuste peut être de passer par un reverse proxy local qui te donne une URL propre et stable, et qui relaie vers ton localhost. C’est plus de plomberie, mais c’est du plumbing qui te fait gagner du temps sur la durée.
En bonus, ça te rapproche d’un setup « quasi-prod » : hostname stable, HTTPS si tu veux, et surtout un seul point d’entrée réseau. Et quand tu dois tester un login SSO ou un callback OAuth sur mobile, tu es content d’avoir déjà un truc carré plutôt qu’une IP LAN bancale.
Garde-fous anti-régression : le test E2E « hash navigation » et la QA mobile avant de bump
Mon avis est simple : si ton app a des docs, une landing longue, une page FAQ, un sommaire, bref n’importe quoi avec des ancres, tu dois avoir un test E2E qui clique un lien vers #id et vérifie que le bon titre est visible. Pas « l’URL contient le hash ». Visible. Sinon tu ne testes pas le bug réel.
Pareil pour le LAN : avant de valider un bump de version, je fais une mini QA sur téléphone. Pas une session d’une heure. Juste ouvrir via Wi‑Fi, vérifier que le JS réagit, et jeter un œil à la console. Le jour où tu dois débugger une UI mobile et que ton dev LAN est cassé, tu perds un temps idiot à bricoler des workarounds. Autant détecter ça quand c’est encore un simple problème de config.
Conclusion : ces régressions ne sont pas « mystérieuses », elles sont juste mal couvertes
Les ancres et le dev sur LAN, c’est typiquement ce que tu ne vois pas dans une PR de feature. C’est du confort, du détail, du « hors happy path ». Et pourtant, c’est exactement ce qui te plombe quand tu dois livrer.
Le bon move, ce n’est pas de devenir parano à chaque bump Next.js. C’est de transformer ces deux points en garde-fous : un fix CSS propre pour les ancres, une config de dev LAN assumée, et deux vérifs automatiques ou semi-automatiques qui te disent tout de suite si tu viens de te faire piéger.