Next.js a une réputation de framework rapide, et il la mérite. Le rendu côté serveur, le découpage automatique du code, l'optimisation des images, tout est là pour livrer vite. Pourtant la majorité des sites Next.js que tu croises rament. Le framework n'est pas en cause. C'est l'usage par défaut qui l'est, parce que Next.js rend si facile d'ajouter de l'interactivité partout que tu finis par envoyer au navigateur trois fois le JavaScript dont tu as besoin.

Sur Copyboost, mon application Next.js déployée sur Vercel envoyait 301 Ko de JavaScript partagé sur chaque page, y compris les pages légales qui n'ont aucune interaction. Voici pourquoi ça arrive, et comment livrer du Next.js qui reste léger.

Le mécanisme : chaque composant client a un coût

Depuis les Server Components, Next.js fait une distinction simple mais lourde de conséquences. Un composant serveur s'exécute au build ou sur le serveur, et arrive au navigateur en HTML déjà rendu, sans JavaScript. Un composant client, lui, embarque son JavaScript, doit être téléchargé, parsé, puis hydraté pour devenir interactif. Cette hydratation occupe le fil principal du navigateur, celui qui doit aussi répondre aux clics de ton visiteur.

Le piège est que la directive qui bascule un composant en mode client se propage. Dès qu'un composant est client, tout ce qu'il importe le devient aussi. Tu ajoutes une petite animation, un menu déroulant, un toggle, et sans le voir tu fais basculer toute une branche de ton arbre côté client. Le JavaScript gonfle, l'hydratation s'allonge, et ta page met plus de temps à répondre. Le visiteur ne voit pas tes Server Components, il subit tes Client Components.

Un exemple concret. Tu as une page de contenu entièrement serveur, donc rapide. Tu veux juste un bouton "copier le code". Tu passes ce bouton en client, c'est légitime. Mais si le bouton vit dans le composant qui rend aussi tout l'article, c'est l'article entier qui bascule côté client. Le navigateur télécharge et hydrate des paragraphes de texte qui n'auront jamais la moindre interaction. La règle n'est pas d'éviter le client, c'est de le poser au bon endroit, le plus bas possible dans l'arbre.

Et ce coût frappe deux fois. Une première sur le LCP, parce que le navigateur occupé à traiter du JavaScript affiche ton contenu plus tard. Une seconde sur l'INP, parce que ce même fil principal saturé par l'hydratation répond en retard aux interactions. Deux des trois Core Web Vitals dégradées par la même cause, du JavaScript client que tu n'avais pas besoin d'envoyer.

La prise de position : le confort par défaut est le piège

La plupart des conseils de performance Next.js te disent quoi optimiser. Le vrai problème est en amont. Next.js est si agréable à utiliser que tu ne ressens jamais le coût de tes choix au moment où tu les fais. Ajouter "use client" en haut d'un fichier prend une seconde et règle ton erreur tout de suite. Personne ne t'arrête pour te dire que tu viens d'envoyer 40 Ko de plus à chaque visiteur.

Donc la discipline ne peut pas venir du framework, elle doit venir de toi. La règle saine est l'inverse du réflexe : tout est serveur par défaut, et un composant ne passe client que si une interaction l'exige vraiment. Pas parce que c'est plus simple sur le moment.

Cette discipline a un retour direct. Chaque kilo-octet de JavaScript que tu n'envoies pas, c'est un fil principal plus libre, un LCP plus court, un clic qui répond. Et pour un SaaS, le clic qui répond au bon moment, c'est l'inscription qui aboutit au lieu de caler sur un bouton mort. C'est la performance, premier levier CRO.

Teardown : Copyboost, le monolithe qui pesait sur tout

Voilà ce que mon propre diagnostic a révélé.

  • Un layout monolithique. Le même layout enveloppait les pages marketing, l'application authentifiée, le blog et les pages légales. Résultat, 301 Ko de JavaScript partagé se chargeaient sur chaque page, même une page de mentions légales sans une seule interaction.
  • Des chunks partagés à tort. Sur 16 chunks JavaScript, 13 étaient communs entre la page d'accueil et l'application. Une page de vente froide tirait le code de l'app authentifiée. La séparation par groupes de routes n'existait pas.
  • Une librairie d'animation chargée globalement. Une librairie de 116 Ko servait uniquement à des effets d'apparition au défilement, et elle était embarquée partout au lieu d'être confinée aux composants de l'app qui en avaient besoin.
  • Neuf polices sans préchargement. Aucune balise de preload, et au passage un doublon de contenu entre le blog français et un blog anglais qui servait le même texte.
  • Un widget de chat chargé globalement. Le composant de chat était monté depuis le layout racine, donc présent et hydraté jusque sur le blog et les pages légales, où il n'avait rien à faire.

Les corrections, par phases : séparation des groupes de routes, isolation de la librairie d'animation aux seuls composants concernés, layout ultra léger pour les pages légales, contrôle du préchargement des polices. Le score Lighthouse est monté de 51 à 62 puis vers 75, le LCP est descendu de 5,4 s vers 4,0 s puis vers la cible de 2,5 s, et le temps de blocage du fil principal est passé de 1740 ms à 950 ms. Aucun changement de design. Juste arrêter d'envoyer du code là où il ne servait à rien.

Comparaison en deux colonnes, le monolithe Next.js avec 301 Ko partout et 13 chunks sur 16 communs, face à l'architecture séparée par groupes de routes avec l'app isolée.
Du layout monolithique aux groupes de routes, le même design, moins de JavaScript.

L'artefact : la checklist Next.js sans alourdir

Avant de livrer, passe ta structure au crible de ces cinq points.

  1. Sépare tes univers par groupes de routes. Le marketing, l'application et les pages légales ne doivent pas partager le même layout ni les mêmes chunks. Une page de vente ne doit jamais tirer le JavaScript de ton app authentifiée.
  2. Serveur par défaut, client par exception. Garde "use client" le plus bas possible dans l'arbre, sur le composant qui interagit vraiment, jamais sur un layout ou une page entière.
  3. Confine tes librairies lourdes. Une librairie d'animation ou de graphes ne se charge que sur les pages qui l'utilisent. Importe la dynamiquement si elle n'est pas visible au dessus de la ligne de flottaison.
  4. Contrôle le préchargement des polices. Avec next/font, les polices sont déjà auto hébergées au build, mais au delà de quelques familles tu dois choisir lesquelles précharger et désactiver le preload des autres pour ne pas saturer.
  5. Audite tes chunks partagés. Regarde la sortie de build de Next.js. Si la page d'accueil et l'app partagent l'essentiel de leurs chunks, ta séparation est cassée, et chaque visiteur paie pour du code qu'il ne verra jamais.

Ce qu'il faut retenir

Next.js ne ralentit pas ton site. C'est la facilité avec laquelle on rend tout interactif qui le fait, un composant client à la fois, jusqu'à ce que le navigateur croule sous du JavaScript inutile. La performance Next.js n'est pas une affaire d'optimisation finale, c'est une affaire de discipline à chaque "use client". Ouvre ta sortie de build, compte ce que tu envoies sur ta page d'accueil, et demande toi combien de ce code sert vraiment à convertir. La plupart du temps, la réponse est gênante, et c'est une excellente nouvelle, parce que ça veut dire que ta page la plus lente est aussi la plus facile à alléger.