Apogée Consult
Retour au blog
Mathieu Ponton
Mathieu PontonCo-Founder & ingénieur logiciel

Je suis Mathieu Ponton, Co-Founder & ingénieur logiciel chez Apogée Consult à Lyon. Ingénieur diplômé de Polytech Lyon (Informatique), j'ai fait trois ans en apprentissage, partagés entre la Métropole de Lyon (inclusion numérique avec Res'in et sobriété énergétique avec Écolyo) et Superwyze, une startup medtech (POCs, dont certains aujourd'hui industrialisés, et travail sur des codebases existantes). J'ai livré plus de 10 projets en production (web, mobile et IA / RAG) pour des PME, startups et organisations publiques.

Bundle JS sous 100 kb sur un site marketing : la discipline qu'on s'impose

  • nextjs

Maintenir un bundle JS sous 100 kb sur un site marketing Next.js demande des décisions constantes. Voici les règles concrètes qu'on applique, avec les outils et les arbitrages.

Bundle JS sous 100 kb sur un site marketing : la discipline qu'on s'impose

Un bundle JS de 100 kb parsé et exécuté représente environ 300ms sur un téléphone milieu de gamme, selon les benchmarks Lighthouse et les mesures v8. Sur un site marketing dont l'objectif est la conversion, c'est du temps perdu avant que l'utilisateur puisse interagir.

On s'est fixé 100 kb de JS total (gzippé) comme limite de budget sur les sites marketing qu'on produit. Ce n'est pas une règle absolue, c'est une contrainte qui force les bonnes questions.

Mesurer avant de couper

On commence chaque audit avec @next/bundle-analyzer :

ANALYZE=true next build

Ça génère un treemap interactif du bundle. Ce qu'on regarde en premier : les gros blocs colorés, les modules dupliqués, et les librairies qui finissent dans le bundle client alors qu'elles pourraient rester côté serveur.

La commande next build produit aussi un tableau récapitulatif des tailles de route dans le terminal. C'est suffisant pour un audit rapide sans graphique.

Règle 1 : les Server Components sont la première défense

En App Router, les composants sont Server Components par défaut. Ils ne contribuent pas au bundle JS client. C'est le mécanisme le plus puissant disponible.

Le réflexe à cultiver : avant d'écrire "use client", se demander si l'interactivité est réellement nécessaire dans ce composant, ou si elle peut être isolée dans un sous-composant plus petit.

Un composant de rendu de contenu riche (markdown, contenu CMS, description produit) n'a généralement pas besoin d'être client. Si on y met "use client" parce qu'un ancêtre en a besoin, on a probablement un problème d'architecture.

Règle 2 : les imports dynamiques pour les features non critiques

Tout ce qui n'est pas nécessaire au premier rendu visible doit être chargé en différé.

import dynamic from "next/dynamic"

// La carte interactive ne charge pas au premier rendu
const ContactMap = dynamic(() => import("@/components/ContactMap"), {
  loading: () => <MapPlaceholder />,
  ssr: false, // pas de rendu serveur pour des composants qui dépendent de window
})

// Le lecteur vidéo uniquement quand l'utilisateur clique
const VideoPlayer = dynamic(() => import("@/components/VideoPlayer"), {
  loading: () => <VideoThumbnail />,
})

La liste des composants qu'on met systématiquement en dynamic sur un site marketing : cartes (Leaflet, Google Maps), lecteurs vidéo, modales lourdes, composants de formulaire complexes (datepicker, rich text editor), scripts d'analytics embarqués dans des composants React.

Règle 3 : auditer les dépendances tierces

Les librairies sont souvent la première source de gonflement involontaire. Quelques exemples de substitutions qu'on fait régulièrement :

  • date-fns complet → import des seules fonctions utilisées (tree-shaking natif)
  • lodashlodash-es avec imports nommés, ou remplacement par du code natif pour les cas simples
  • framer-motion complet → motion/react (la version légère lancée en 2024) ou CSS transitions pour les animations simples
  • react-icons avec import de l'ensemble du pack → import du seul fichier SVG nécessaire

Pour chaque nouvelle dépendance ajoutée, on consulte bundlephobia.com avant d'accepter la PR. C'est une règle d'équipe, pas une vérification optionnelle.

Règle 4 : ne pas embarquer côté client ce qui peut rester serveur

C'est une extension de la règle 1, mais au niveau des librairies. Certaines librairies font à la fois du traitement de données et du rendu, on peut souvent séparer les deux.

Exemple concret : une librairie de génération de PDF. Si on génère des PDFs uniquement sur action utilisateur et côté serveur, on n'a pas à l'embarquer dans le bundle client. On l'appelle depuis une Server Action ou une route API.

// Mauvais : la librairie PDF dans le bundle client
"use client"
import { generatePDF } from "heavy-pdf-library"

// Correct : la librairie reste serveur
"use server"
import { generatePDF } from "heavy-pdf-library"
export async function exportPDF(data) {
  return generatePDF(data) // s'exécute côté serveur uniquement
}

Ce qu'on sacrifie pour tenir la limite

La limite de 100 kb implique des choix inconfortables. On renonce à certaines librairies d'UI populaires qui embarquent trop de JS par défaut. On évite les composants d'animation complexes sur la homepage si leur impact bundle dépasse 15 kb. On préfère parfois une animation CSS à une animation JavaScript équivalente.

Sur un projet récent, on a refusé l'intégration d'un widget chat tiers proposé par le client, 47 kb gzippés, chargés au démarrage, bloquants pour l'interactivité. On a proposé à la place un chargement différé déclenché par l'interaction sur le bouton chat. Le client a accepté.

Surveiller la dérive dans le temps

Le budget ne tient que si on le surveille activement. On a intégré dans notre CI une vérification du bundle size :

# Dans le workflow GitHub Actions
- name: Check bundle size
  run: |
    BUNDLE_SIZE=$(next build 2>&1 | grep "First Load JS" | awk '{print $NF}' | sort -n | tail -1)
    echo "Bundle size: $BUNDLE_SIZE"

C'est rudimentaire, mais ça force la question à chaque merge. Des outils comme bundlewatch ou size-limit offrent une intégration plus fine avec des seuils configurables et des commentaires automatiques sur les PRs.

La question qu'on débat encore en interne : 100 kb est-il le bon seuil, ou faut-il raisonner en temps de parsing plutôt qu'en poids ? Un bundle de 80 kb de WebAssembly n'a pas le même coût CPU qu'un bundle de 80 kb de JavaScript classique.

Disponible pour de nouveaux projets

Un projet à concrétiser ?
Parlons-en, sans engagement.

Un échange de 30 minutes pour cadrer votre besoin, qualifier la faisabilité et vous proposer une trajectoire claire.

1// kick-off : réponse sous 24h
2const project = await apogee.scope({
3 type: 'web | mobile | IA',
4 timeline: '6 à 16 semaines',
5 approach: 'sur-mesure'
6})
7// → cadrage offert
Bundle JS Next.js sous 100 kb : nos pratiques | Apogée Consult