Accueil / Articles / La découverte des ressources, pas leur transfert : le préchargement des applications React via HTTP/3

La découverte des ressources, pas leur transfert : le préchargement des applications React via HTTP/3

Découvrez pourquoi HTTP/3 fait de la découverte tardive des ressources le véritable goulot d’étranglement dans les applications React, et comment preloadModule, preinit, Early Hints et le chunking y remédient.

4462 mots

Mettre à jour un serveur pour qu’il utilise HTTP/3 permet aux données de se déplacer plus rapidement, mais de nombreuses applications React ne semblent guère plus rapides par la suite. La raison habituelle est que la partie lente du processus n’a jamais été le transfert lui-même : c’était plutôt le moment où le navigateur apprenait même l’existence d’une ressource. Ce guide distingue ces deux problèmes, montre où QUIC est réellement utile, et présente les outils qui permettent d’avancer la phase de découverte : les API de ressources de React 19, le préchargement de modules basé sur des intentions, les indications 103 Early Hints, le SSR en streaming ainsi qu’une stratégie de segmentation adaptée au transport multiplexé.

Un flux qui semble correct mais reste lent

Imaginez une équipe qui analyse un tableau de bord analytique via une connexion 4G. Chaque élément essentiel est déjà préchargé, la structure du réseau semble ordonnée, et pourtant le temps nécessaire pour atteindre l’état interactif reste obstinément en deçà de l’objectif. Chaque élément se télécharge rapidement une fois le téléchargement commencé, ce qui montre clairement que HTTP/3 remplit sa fonction.

En y regardant de plus près, le schéma change. Les téléchargements sont rapides, mais les demandes commencent tard. React doit se télécharger, démarrer, s’exécuter et afficher du contenu jusqu’à atteindre une limite de chargement différé, et ce n’est qu’à ce moment-là que le navigateur découvre qu’il a besoin de Dashboard.js. Des centaines de millisecondes se sont écoulées avant même que le premier octet de cet élément ne soit demandé. Le retard provient en amont du réseau, lors d’une étape que la plupart des listes de contrôle de performance n’évoquent jamais séparément : la découverte des ressources, par opposition à leur livraison.

La livraison et la découverte sont deux problèmes distincts

Il est utile de diviser « le chargement d’une ressource » en deux questions :

  • Vitesse de transfert : une fois la demande faite, à quelle vitesse les octets voyagent-ils du serveur au navigateur ?
  • Délai de découverte : à quel moment le navigateur se rend-il compte qu’il a besoin de ces octets en premier lieu ?

Un police web référencé depuis une feuille de style rend la différence évidente. La chaîne s’organise comme suit :

HTML → CSS → @font-face rule → font request

Peu importe la vitesse de la connexion, la demande de police ne peut être envoyée qu’une fois que l’HTML est arrivé, que le CSS a été téléchargé et analysé, et que la règle @font-face a été trouvée et comparée au texte affiché. Il s’agit donc d’un délai de découverte. Une balise <link rel="preload"> ne accélère pas le téléchargement de la police d’un seul milliseconde ; elle permet simplement au navigateur d’envoyer la demande plus tôt. Presque tout ce qui est abordé dans le reste de ce guide relève d’une variation de cette idée.

Quel outil répond à quelle question

Chaque technologie présentée ci-dessous vise une étape différente du processus de chargement :

  • preconnect : avec quels origines le navigateur doit-il commencer à communiquer avant même qu’une demande ne soit formulée ?
  • preload : quel fichier spécifique le navigateur risque-t-il de trouver trop tard par lui-même ?
  • preloadModule et modulepreload : quels fragments de modules ES doivent être téléchargés et compilés à l’avance avant l’exécution ?
  • preinit et preinitModule : quels ressources doivent non seulement être téléchargées mais aussi appliquées ou exécutées tôt ?
  • prefetch : quels éléments seront probablement nécessaires lors de la prochaine navigation, avec une faible priorité ?
  • 103 Early Hints : que sait déjà le serveur avant que l’HTML ne soit prêt ?
  • Streaming SSR : comment le serveur peut-il révéler progressivement le contenu ainsi que les ressources qui le composent ?
  • HTTP/3 et QUIC : une fois une requête envoyée, comment ses octets sont-ils transmis de manière efficace ?
  • Seul le dernier point concerne le transport. Tout le reste porte sur la découverte, le timing ou le planification, et cette proportion donne une image fidèle des domaines où se trouvent généralement les autres avantages.

    Où le multiplexage d’HTTP/2 a échoué

    Sous HTTP/1.1, les navigateurs atteignaient le parallélisme en ouvrant plusieurs connexions TCP par hôte, généralement limitées à environ six, chaque connexion transportant une ressource à la fois. HTTP/2 a remplacé cela par une seule connexion qui intercale de nombreux flux :

    HTTP/1.1                     HTTP/2
    ────────────────             ────────────────────
    TCP conn 1 → JS              One connection
    TCP conn 2 → CSS               ├── Stream A: JS
    TCP conn 3 → Font              ├── Stream B: CSS
    TCP conn 4 → Image             ├── Stream C: Font
                                   └── Stream D: Image
    

    C’était un véritable progrès, mais HTTP/2 reposait toujours sur TCP, qui garantit une livraison strictement ordonnée d’un flux de bytes unique. Il ne sait pas que certains bytes appartiennent au flux JavaScript et d’autres au flux de polices. Si un paquet disparaît, TCP retient tout ce qui suit jusqu’à l’arrivée de la retransmission, même lorsque le paquet perdu faisait partie du Flux C et que les trois autres flux n’y étaient pas concernés.

    C’est ce qu’on appelle le blocage en tête de file (HOL blocking) de TCP. Sur des réseaux stables, cela est rarement perceptible ; sur des liaisons mobiles sujettes aux pertes, c’est la raison principale pour laquelle le multiplexage d’HTTP/2 n’a jamais pleinement tenu ses promesses.

    Quelles sont les modifications apportées par QUIC sous HTTP/3

    HTTP/3 remplace TCP par QUIC, qui fonctionne sur UDP et gère lui-même le chiffrement :

    HTTP/2          HTTP/3
    ────────        ────────
    HTTP/2          HTTP/3
      ↓               ↓
     TCP            QUIC
      ↓               ↓
     TLS            UDP
      ↓               ↓
     IP             IP
    

    Les flux indépendants éliminent le blocage au niveau du transport

    Puisque QUIC gère les flux au sein du protocole de transport, chaque flux est récupéré de manière indépendante. Un paquet perdu ne bloque que le flux auquel il appartient :

    Stream A ──────────────────── ✓
    Stream B ──────────────────── ✓
    Stream C ──────── X ─ retry
    Stream D ──────────────────── ✓
    

    Les flux A, B et D continuent de s’écouler tandis que C attend sa retransmission. L’effet mesuré est le plus important précisément là où TCP a subi les pires pertes. Une étude Catchpoint publiée en juillet 2025, menée dans six pays, a révélé que, sur des liens fortement sujets à des pertes de données, le temps moyen jusqu’au premier octet avait diminué de 41,8 %. Des tests internes chez Wix ont montré que l’installation de la connexion était 33 % plus rapide et que le temps p75 LCP s’était amélioré de 20 %. Sur un lien à large bande stable, l’avantage par rapport à HTTP/2 diminue à environ 5 %. Cette asymétrie est en soi révélatrice : les gains se concentrent là où le blocage HOL avait un impact négatif. Considérez ces chiffres comme des extraits de ces études plutôt que comme des garanties pour votre trafic, et mesurez vous-même vos utilisateurs.

    Avec HTTP/2 sur TCP, une nouvelle connexion nécessite d’abord l’échange de paquets pour établir la connexion TCP, suivi d’une négociation TLS distincte, ce qui représente deux allers-retours avant que des données d’application ne puissent circuler. QUIC intègre l’échange TLS 1.3 dans la mise en place de la connexion et les termine tous deux en un seul aller-retour. Pour les visiteurs récurrents, la reprise 0-RTT permet aux données chiffrées des requêtes d’être transmises avec le paquet d’ouverture. Sur une liaison intercontinentale de 150 ms, cela permet d’économiser entre 150 et 300 ms pour chaque connexion nouvelle. Il convient de noter que les données 0-RTT peuvent être rejouées, c’est pourquoi les serveurs les acceptent généralement uniquement pour des requêtes idempotentes telles que la récupération d’assets statiques.

    Connexions qui survivent à un changement de réseau

    TCP identifie une connexion grâce à la combinaison des adresses IP source et destination ainsi que des ports. Lorsqu’un téléphone passe du Wi-Fi au réseau mobile, son adresse change et la connexion TCP est interrompue. En revanche, QUIC utilise un ID de connexion opaque, ce qui permet à la session de migrer vers le nouveau chemin. Un bloc de route en préchargement n’a pas besoin d’être reprise à zéro simplement parce que le train d’un pendulaire quitte la gare.

    HTTP/3 rend-il le préchargement superflu ?

    Non, et comprendre pourquoi constitue l’essence de tout ce sujet. HTTP/3 optimise la manière dont les ressources sont transmises ; le préchargement optimise le moment où elles sont demandées. Les deux interviennent à des étapes différentes de la chaîne :

    Browser
      │
      │  ← "I don't know I need this yet"
      ↓
    Resource discovery    ← preload operates here
      │
      ↓
    Request
      │
      ↓
    QUIC transport        ← HTTP/3 operates here
      │
      ↓
    Server
    

    Un protocole de transport ne peut pas récupérer quelque chose que personne n’a encore demandé. En fait, un transport plus rapide rend la découverte tardive encore plus évidente. Supposons que le temps de transfert d’un bloc passe de 300 ms à 80 ms. Un délai de découverte de 400 ms, qui était auparavant partiellement masqué par le temps total, constitue désormais la majeure partie de celui-ci. Le goulot d’étranglement a simplement changé de place sans disparaître.

    Pourquoi React cache les dépendances du navigateur

    Les ressources HTML simples telles que les sources <img> et les feuilles de style <link> sont détectées tôt, car le parseur du navigateur (ainsi que son outil de préchargement spéculatif) les repère lors de la lecture du document. React, géré côté client, crée une chaîne bien plus longue avant que certaines dépendances ne deviennent visibles :

    HTML → main.js → React executes → render → lazy() → discover Dashboard.js → download → render
    

    Avec React.lazy(), l’import a lieu après l’exécution du JavaScript. Le navigateur ne peut pas savoir que Dashboard.js existe avant que le bundle principal n’ait été téléchargé, analysé, compilé et exécuté, et que React n’ait rendu suffisamment de contenu pour atteindre le composant différé. Lors d’une première visite depuis un téléphone lent, cela peut signifier plusieurs secondes avant que la demande du chunk ne commence.

    const Dashboard = lazy(() => import("./Dashboard"));
    // The browser has no idea Dashboard.js exists
    // until this renders. And it only renders after
    // React has fully bootstrapped.
    

    Suspense améliore l’attente, pas la découverte

    Une idée reçue courante est que l’enveloppement du composant dans Suspense résout le problème. Cela ne change rien au moment où le chunk est demandé :

    <Suspense fallback={<Loading />}>
      <Dashboard />
    </Suspense>
    

    Ce que Suspense offre, c’est une coordination : tant que le composant différé n’a pas été chargé, React affiche une alternative plutôt que de bloquer toute la structure. Cela améliore la qualité perçue, mais ce n’est pas un mécanisme de prédiction des ressources. La demande est toujours lancée au même moment tardif. HTTP/3 permettra ensuite de transmettre les données efficacement, mais il n’a aucune influence sur le temps nécessaire pour y parvenir.

    Les API de ressources de React 19 et ce que fait réellement chacune d’elles

    React 19 intègre dans react-dom une série de fonctions qui permettent aux composants d’envoyer des indications sur les ressources au planificateur du navigateur, précisément au moment où le besoin en devient apparent pendant le rendu. Il s’agit de bien plus que de simples enveloppes autour des balises HTML : React les dédoublonne, et lors du rendu serveur, il peut les insérer dans la partie en-tête du document afin que le navigateur les perçoive tôt.

    preconnect : préchauffer un origine

    Utilisez preconnect lorsque une requête cross-origin doit certainement suivre prochainement. Il lance à l’avance la résolution DNS, la connexion ainsi que le handshake TLS.

    import { preconnect } from "react-dom";
    // Call this when you know a cross-origin
    // request is coming - not just "might be coming."
    preconnect("https://cdn.example.com");
    

    Réservez-le aux origines que vous allez absolument contacter. Chaque connexion préchauffée entraîne des efforts tant du côté client que serveur, et une connexion inutilisée est simplement abandonnée.

    preload : télécharger un fichier spécifique en avance

    preload indique au navigateur de commencer à télécharger une ressource connue sans l’exécuter ni l’appliquer. Les polices sont le cas typique, car elles seraient sinon masquées par l’analyse du CSS :

    import { preload } from "react-dom";
    // Font hidden behind CSS - the browser won't
    // find this until it processes @font-face.
    // Preload surfaces it earlier.
    preload("/fonts/inter.woff2", {
      as: "font",
      crossOrigin: "anonymous",
    });
    

    Prêtez attention à l’option crossOrigin: "anonymous". Les polices sont toujours demandées en mode CORS, donc un preload de police sans cette option génère une requête qui ne correspond pas à la vraie, ce qui amène le navigateur à télécharger le fichier deux fois.

    preloadModule : récupérer et compiler un module ES

    preloadModule a la même finalité pour les modules ES, mais va plus loin : le module est téléchargé, analysé et compilé, puis conservé dans sa carte de modules, afin de pouvoir être évalué dès que l’instruction import() en fait la demande.

    import { preloadModule } from "react-dom";
    // Use this for lazy route chunks you know
    // are likely to be needed soon.
    preloadModule("/assets/Dashboard-abc123.js");
    

    C’est l’option idéale pour les blocs de route différés qui risquent d’être nécessaires prochainement.

    preinit et preinitModule : récupérer et mettre en œuvre

    preinit et preinitModule sont les variantes plus puissantes. Elles récupèrent la ressource et s’assurent également qu’elle soit mise en œuvre : une feuille de style est insérée et appliquée, et un script est exécuté dès son arrivée.

    import { preinit } from "react-dom";
    // You don't just want this downloaded -
    // you want it applied before render.
    preinit("/styles/app.css", { as: "style" });
    

    L’écart entre les deux familles est particulièrement important pour CSS. Un fichier de style préchargé est téléchargé mais n’est pas appliqué. Si ce fichier de style est nécessaire avant la première affichage, vous avez déplacé le téléchargement plus tôt, mais le rendu attend toujours que quelque chose l’insère réellement. preinit couvre ces deux étapes. Pour les scripts, la prudence est inverse : seuls les codes preinit pouvant être exécutés immédiatement sont appropriés.

    Déclencher preloadModule en fonction de l’intention de l’utilisateur

    Appeler preloadModule pour chaque route au démarrage gaspille de la bande passante. Le meilleur moment est lorsque l’intention de l’utilisateur devient visible, ce qui signifie généralement qu’un curseur entre dans un lien de navigation ou que le focus clavier se pose dessus juste avant le clic.

    Le composant ci-dessous gère cela. Il affiche un lien normal afin que celui-ci fonctionne toujours sans JavaScript, appelle preloadModule tant lors de l’événement onMouseEnter que de onFocus pour que les utilisateurs du clavier puissent également en bénéficier, et confie la navigation réelle à navigate de React Router :

    import { preloadModule } from "react-dom";
    import { useNavigate } from "react-router-dom";
    
    function NavLink({ to, chunkPath, children }) {
      const navigate = useNavigate();
      return (
        <a
          href={to}
          onMouseEnter={() => preloadModule(chunkPath)}
          onFocus={() => preloadModule(chunkPath)}
          onClick={(e) => {
            e.preventDefault();
            navigate(to);
          }}
        >
          {children}
        </a>
      );
    }
    
    // Usage
    <NavLink to="/dashboard" chunkPath="/assets/Dashboard-abc123.js">
      Dashboard
    </NavLink>
    

    Le délai entre le survol et le clic est généralement d’environ 100 à 400 ms. Avec HTTP/3, un fichier de taille moyenne peut souvent être chargé dans cette fenêtre temporelle : à 10 Mbps, 150 KB nécessite environ 120 ms. Au moment où le clic est effectué, le module est déjà compilé dans la carte des modules, la limite de chargement différé est résolue immédiatement, et le mécanisme de fallback Suspense n’apparaît jamais.

    Ce que le gestionnaire onMouseEnter permet d’accomplir est quelque chose que aucun protocole de transport ne peut faire : il transforme un signal d’intention en informations sur une ressource avant même que la navigation ne soit demandée. HTTP/3 gère ensuite le transfert de manière efficace. Chaque couche s’occupe de sa tâche propre.

    Deux remarques pratiques. Premièrement, chunkPath doit correspondre au nom de fichier haché généré réellement par votre outil de bundling ; dans un projet réel, il devrait provenir du manifeste de compilation plutôt que d’être saisi manuellement. Deuxièmement, les appareils tactiles ne disposent pas de fonction de survol, il convient donc d’envisager onTouchStart ou des déclencheurs basés sur la vue d’écran si la navigation mobile est importante pour vous.

    Préchargement au niveau de l’outil de bundling

    Pour un préchargement en masse à faible priorité pendant les périodes d’inactivité, webpack prend en charge un commentaire spécial à l’intérieur de l’import dynamique :

    const Dashboard = lazy(
      () => import(/* webpackPrefetch: true */ "./Dashboard")
    );
    

    Il convient de noter que webpackPrefetch génère un élément <link rel="prefetch">, que le navigateur traite comme une tâche à faible priorité effectuée en temps d’inactivité, destinée à une navigation future probable. Il s’agit d’un signal différent de preload ou modulepreload, à haute priorité, utilisés pour les ressources nécessaires immédiatement.

    Vite adopte une approche plus automatique en générant pour vous des liens modulepreload. Avec webpack, il faut utiliser un commentaire spécial ou un plugin. La forme HTML native de cet indicateur est la suivante :

    <!-- Vite generates these for lazy chunks automatically -->
    <link rel="modulepreload" href="/assets/Dashboard-abc123.js">
    <link rel="modulepreload" href="/assets/vendor-react-def456.js">
    

    En termes stricts, l’HTML généré par Vite contient des liens modulepreload pour le chunk d’entrée et ses imports statiques. Pour les chunks importés dynamiquement, l’outil de runtime de Vite insère des liens preload pour leurs dépendances au moment où l’instruction import() est exécutée, de sorte que le chunk et ses imports s’chargent en parallèle plutôt qu’en séquence. Consultez la documentation de construction de votre version de Vite pour connaître le comportement exact.

    Par rapport à un rel="preload" générique, modulepreload permet au navigateur de parser et de compiler le module dès son arrivée, plutôt que d’attendre le moment de l’exécution. Sur HTTP/3, plusieurs de ces indications sont transmises via des flux QUIC indépendants, ce qui évite que la perte d’un paquet dans le chunk du fournisseur ne ralentisse le chargement du chunk du tableau de bord.

    De Server Push aux indications précoce 103

    HTTP/2 a tenté de résoudre le problème de la découverte du contenu du côté serveur grâce à Server Push : le serveur envoyait des ressources que le navigateur n’avait pas encore demandées. L’objectif de déplacer cette fonctionnalité plus tôt était judicieux, mais sa mise en œuvre a échoué. Le serveur ne disposait d’aucun moyen fiable pour savoir si le navigateur avait déjà en mémoire cache une ressource, ce qui a souvent conduit à l’envoi de duplicatas, à une consommation inutile de bande passante et à des conflits avec d’autres ressources que le navigateur jugeait plus urgentes. Chrome a finalement abandonné son support pour Server Push.

    Le modèle qui l’a remplacé répartit les responsabilités de manière plus logique : le serveur fournit des informations, tandis que c’est le navigateur qui décide ce qu’il faut télécharger et quand.

    103 Early Hints mettent cela en pratique. Tandis que le serveur est encore en train de préparer la réponse principale, il envoie un statut provisoire 103 accompagné de en-têtes Link. Le navigateur peut commencer à récupérer ces ressources immédiatement, et au moment où la réponse finale 200 OK contenant l’HTML arrive, certaines d’entre elles pourraient déjà être prêtes.

    Browser                    Server
      │                          │
      │──── GET / ─────────────→ │
      │                          │ (generating HTML...)
      │ ←─── 103 Early Hints ─── │
      │   Link: </assets/main.js>; rel=modulepreload
      │   Link: </assets/vendor.js>; rel=modulepreload
      │                          │
      │ (fetching chunks now...) │ (still generating...)
      │                          │
      │ ←─── 200 OK + HTML ───── │
      │   (chunks already downloading or done)
    

    Au moment de la rédaction, NGINX intègre déjà un support natif pour Early Hints depuis sa version 1.29.0 en juin 2025, et Cloudflare le met à disposition sous forme d’option dans son tableau de bord. Dans Node.js, l’objet réponse propose la méthode writeEarlyHints(), que vous pouvez appeler dans un serveur ou un middleware personnalisé avant d’envoyer la véritable réponse :

    // In a custom server or middleware
    res.writeEarlyHints({
      link: [
        "</assets/main.js>; rel=modulepreload; as=script",
        "</assets/vendor.js>; rel=modulepreload; as=script",
        "</assets/Dashboard.js>; rel=modulepreload; as=script",
      ],
    });
    
    // Then proceed with normal response
    res.status(200).send(html);
    

    Cette partie de code utilise une méthode de style Express res.status().send() pour la réponse finale. Avec un serveur Node.js basique http, on utiliserait plutôt res.writeHead() et res.end(). Les Early Hints ne sont utiles que lorsqu’il y a un véritable temps de traitement du serveur, comme pour des requêtes à la base de données ou du rendu, période pendant laquelle le navigateur resterait sinon inactif.

    Une mesure publiée par corewebvitals.io, utilisant les chronométrages des outils de développement Chrome, a montré que l’inclusion d’un fichier CSS critique via les Early Hints faisait apparaître l’élément LCP environ 35 % plus tôt que lors d’un préchargement conventionnel à l’intérieur de l’HTML. Pour une application React, l’avantage équivalent consiste à permettre le téléchargement du chunk principal tant que le serveur est encore en activité, plutôt qu’après l’arrivée de l’HTML.

    Combinaison du SSR en streaming, des Early Hints et d’HTTP/3

    Le rendu serveur en flux de React ajoute un autre levier. Au lieu d’attendre que la page complète soit prête pour envoyer la réponse, le serveur envoie l’HTML par étapes :

    HTML shell → Suspense fallback → more HTML → resolved content → hydration
    

    Lors du rendu, le serveur sait quels conteneurs Suspense vont être rendus et de quels fragments ils dépendent. Ces informations peuvent être transmises au navigateur via des Early Hints avant même que le flux HTML ne commence. Un schéma simplifié :

    0ms  ── Browser sends request
         ── Server starts rendering
    1ms  ── Server knows Dashboard boundary will render
         ── Server sends 103 Early Hints: Dashboard.js
         ── Browser starts fetching Dashboard.js
    50ms ── Server streams HTML shell
         ── Browser starts parsing
    120ms── Server streams Dashboard content
         ── Dashboard.js already downloaded
         ── Hydration starts immediately
    

    Comparez avec la même application sans Early Hints, où la découverte des éléments attend le client :

    0ms  ── Browser sends request
    50ms ── Server streams HTML shell
         ── Browser starts parsing
         ── Browser discovers <script> tags
         ── main.js starts downloading
    180ms── React executes
         ── Hits Dashboard lazy boundary
         ── Dashboard.js request starts (now)
    300ms── Dashboard.js downloads
         ── Hydration starts
    

    HTTP/3 raccourcit chaque segment dans les deux types de flux. Ce que modifient les Early Hints, c’est le moment où la demande vers le Dashboard commence, ce qui représente une économie différente et souvent plus importante. Si le rendu de votre framework est déjà géré par d’autres primitives, l’article sur les primitives SSR de React 19.2 telles que Activity et le pré-rendering partiel explique plus en détail comment les limites de streaming sont définies.

    Réfléchir à la granularité des blocs pour le transport multiplexé

    Le conseil de longue date visant à réduire le nombre de requêtes HTTP provient de la limite de six connexions d’HTTP/1.1 ainsi que du fait que le multiplexage d’HTTP/2 n’est qu’en partie parallèle au sein d’une seule flux TCP. Avec des flux QUIC indépendants, le nombre de requêtes cesse d’être le facteur dominant qu’il était auparavant, ce qui permet de diviser les fichiers de manière plus agressive sans subir la même pénalité par requête.

    Une répartition par défaut judicieuse se présente comme suit :

    • React et ReactDOM : un chunk dédié et stable fourni par le fournisseur. Il change rarement, ce qui lui permet d’avoir une longue durée de vie dans le cache.
    • Bibliothèque Router : son propre chunk, pour les mêmes raisons de stabilité.
    • Bibliothèques tierces lourdes telles que des outils de visualisation ou des éditeurs : un chunk par bibliothèque, afin que la mise à jour d’une ne rende pas obsolètes les autres.
  • Composants de route : un chunk par route via React.lazy(), de sorte que la navigation ne charge que ce dont elle a besoin.
  • Outils partagés : un seul chunk partagé généré par le bundler, chargé une seule fois et réutilisé entre les routes.
  • Fonctionnalités administratives ou peu utilisées : des chunks lazy distincts que les utilisateurs ne consultant jamais ces écrans ne téléchargent jamais.
  • Le bénéfice réside dans une précision du cache. Modifier Dashboard.tsx devrait vider le cache pour ce chunk de route spécifique, tandis que le bundle fournisseur reste dans le cache, et cela n’est possible qu’avec une segmentation fine. Avec HTTP/3, les 8 à 15 chunks résultants se chargent sur des flux indépendants sans blocage HOL entre eux.

    Vite gère la plupart de ces cas sans configuration. Dans webpack, splitChunks.cacheGroups exprime la même politique. La configuration ci-dessous crée un chunk destiné aux bibliothèques React, un chunk pour le routeur, ainsi qu’un chunk uniquement asynchrone pour les graphiques ; les valeurs priority déterminent quel groupe l’emporte lorsque un module correspond à plusieurs critères, et chunks: "async" empêche le code de visualisation des graphiques d’être chargé au départ :

    // webpack.config.js
    module.exports = {
      optimization: {
        splitChunks: {
          cacheGroups: {
            reactVendor: {
              test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
              name: "vendor-react",
              chunks: "all",
              priority: 40,
            },
            routerVendor: {
              test: /[\\/]node_modules[\\/](react-router|react-router-dom)[\\/]/,
              name: "vendor-router",
              chunks: "all",
              priority: 30,
            },
            chartsVendor: {
              test: /[\\/]node_modules[\\/](recharts|d3)[\\/]/,
              name: "vendor-charts",
              chunks: "async",
              priority: 20,
            },
          },
        },
      },
    };
    

    Il y a un bémol : de très petits fragments augmentent la charge liée à l’analyse et à la compilation de chaque fichier dans le navigateur. De plus, tous les visiteurs n’ont pas accès à HTTP/3 : les pare-feu d’entreprise qui bloquent UDP sur le port 443 sont courants, et ces utilisateurs doivent recourir à HTTP/2, ce qui réduit en partie les coûts liés au nombre de requêtes. Mesurez avant de diviser le contenu en fragments encore plus petits que au niveau des routes. Une granularité par route ou par bibliothèque lourde est généralement appropriée, tandis qu’une granularité par composant ne l’est pas. Si vous utilisez Next.js, l’article sur les contrôles de segmentation de Turbopack aborde les paramètres équivalents dans ce cadre.

    Pourquoi le préchargement de tout peut encore avoir des effets négatifs

    Le multiplexage permet à de nombreux flux d’utiliser une même connexion, mais il ne leur accorde pas tous une bande passante illimitée ni ne les rend équivalentement importants. Vingt indications modulepreload partagent toujours un seul canal ; la concurrence est simplement répartie sur davantage de flux.

    La question pertinente n’est donc pas « que peut être préchargé ? », mais plutôt « quel ressource importante le navigateur ne remarquera-t-il que trop tard ? » Comme point de départ, considérez :

    • Police critique : preload.
    • Fichier de style critique : preload, ou preinit lorsqu’il doit être appliqué avant l’affichage.
    • Module JavaScript critique : preloadModule.
    • Origine CDN ou API importante entre origines différentes : preconnect.
  • Image principale affichée au chargement : preload avec fetchpriority="high".
  • Images plus bas dans la page : pas de préchargement.
  • Bloc de route différé : preloadModule, déclenché par l’intention de l’utilisateur.
  • Scripts d’analyse : pas de préchargement.
  • Widget de chat : chargement différé sans préchargement.
  • Codage réservé aux administrateurs : pas de préchargement.
  • Navigation suivante probable : prefetch à faible priorité.
  • Ressources critiques déjà connues du serveur : 103 Early Hints.
  • Lisez chaque entrée comme une « considération » et non comme une règle stricte. Il n’existe pas de liste universelle de préchargement, seulement des ressources à la fois importantes et découvertes tardivement dans votre application spécifique.

    La même réserve s’applique à fetchpriority. Les navigateurs donnent déjà la priorité aux ressources en utilisant des heuristiques sophistiquées, et marquer tout en high revient à ne rien marquer. Surchargez la valeur par défaut uniquement lorsque des mesures montrent que le navigateur se trompe.

    Cinq questions pour évaluer une stratégie de chargement

    Lorsque vous analysez la manière dont une application React charge ses ressources, examinez successivement les questions suivantes :

    1. À quel moment cette ressource est-elle découverte ? Si la réponse honnête est « après l’exécution du JavaScript » ou « après le rendu de React », il y a probablement moyen de la rendre accessible plus tôt.
    2. À quel moment l’utilisateur en a-t-il besoin ? Quelque chose peut être important sans être nécessaire immédiatement. Cette distinction permet de choisir entre preload (dès maintenant) et prefetch (pendant les périodes d’inactivité).
  • Ces connaissances peuvent-elles être transmises plus tôt ? En ordre approximatif croissant de complexité : preconnect, puis preload ou preloadModule, ensuite preinit, puis les 103 indications précoce, enfin le streaming SSR avec des indications générées par le serveur à partir de ce qu’il rend.
  • Avec quoi cela concurrence-t-il ? Chaque indication a un coût d’opportunité. Le préchargement du bloc Dashboard signifie que quelque autre élément reçoit moins d’attention, il faut donc savoir de quoi il s’agit.
  • La contrainte vient-elle du réseau ou de la CPU ? Le préchargement d’un paquet de 2 MB avance son téléchargement, mais n’affecte en rien le temps de parsing, de compilation et d’exécution. Si c’est la thread principale qui constitue la contrainte, une découverte plus précoce ne change que le moment où l’utilisateur attend, pas la durée de cet attente.
  • Comment les différents niveaux s’articulent-ils

    En examinant l’ensemble du processus, le chargement d’une application React moderne implique six couches, chacune ayant son propre champ d’application :

    Application intent (React knows which routes and components are needed)
           ↓
    Resource APIs (preconnect / preload / preloadModule / preinit)
           ↓
    Server-side surfacing (103 Early Hints / streaming SSR)
           ↓
    Browser resource scheduler (priority, cache, bandwidth estimation)
           ↓
    HTTP/3 / QUIC transport (independent streams, 0-RTT, connection migration)
           ↓
    Network
    

    La méthode ancienne consistait à envoyer autant que possible des ressources au client : Server Push, préchargement de tout, et regroupement des bundles afin de réduire le nombre de requêtes. La méthode actuelle consiste à fournir au navigateur des informations plus détaillées à chaque couche et à le laisser gérer les appels de planification. HTTP/3 offre un transport capable d’agir efficacement sur ces décisions. Les Early Hints transmettent des informations provenant du serveur au navigateur avant même que l’HTML ne soit généré. Les API de ressources de React permettent à l’application d’indiquer ses intentions au moment où un composant est chargé.

    Aucun niveau ne peut remplacer un autre. HTTP/3 ne peut pas récupérer ce qui n’a pas encore été découvert. Les Early Hints sont inutiles lorsque le serveur ne sait pas quels chemins doivent être affichés. De plus, preloadModule ne peut pas sauver un bundle monolithique de 2 MB qui nécessite 800 ms pour se compiler sur un téléphone de milieu de gamme.

    Points clés

    L’effet majeur d’HTTP/3 sur le préchargement n’est pas une vitesse de transfert plus élevée ; c’est plutôt le fait que des transferts plus rapides rendent la découverte tardive relativement plus coûteuse. Lorsqu’un transfert de 400 ms devient de 80 ms, le délai de découverte qui était auparavant masqué par ce temps devient le coût dominant, et une stratégie adaptée à l’ancien goulot d’étranglement ne sert plus à rien.

    • Précharger une ressource parce que le navigateur la découvrirait sinon trop tard, et non simplement parce qu’elle est importante. Les ressources importantes découvertes à temps n’ont pas besoin d’indication, tandis que celles peu importantes découvertes tardivement ne le méritent pas non plus.
    • Suspense améliore ce que l’utilisateur voit pendant l’attente ; il ne fait pas charger les données plus tôt.
    • Choisissez l’indication la moins intrusive qui fonctionne : preconnect pour les origines, preload pour les fichiers, preloadModule pour les modules, preinit lorsque quelque chose doit être appliqué ou exécuté.
    • Déclenchez le préchargement par route à partir de signaux d’intention tels que l’effet hover ou focus, et laissez les Early Hints ainsi que le streaming SSR révéler ce que le serveur sait déjà.
    • Séparez les préchargements par route et par bibliothèque lourde, gardez à l’esprit le fallback HTTP/2, et mesurez avant d’aller plus en détail.

    HTTP/3 n’a pas rendu le préchargement obsolète. Il a plutôt clarifié à quoi sert ce dernier.

    Lectures complémentaires