Au-delà de la taille du paquet : identifier ce qui ralentit réellement votre application web
Pourquoi rayer des kilo-octets ne résout que rarement un problème de lenteur d’une application, et comment suivre le temps réel d’attente à travers les serveurs, les flux en cascade, les mécanismes d’hydratation, les scripts de tiers et les images.
Une scène familière se reproduit fréquemment au sein des équipes frontend : des semaines sont consacrées à réduire de 40 KB un bundle JavaScript, tandis qu’une requête à la base de données prenant 900 millisecondes reste inchangée sur le chemin d’exécution critique. Une bibliothèque d’icônes est remplacée, une dépendance modifiée, un autre plugin de bundling configuré, et un débat éclate quant au fait qu’un paquet pèse 18 KB ou 12 KB. Puis quelqu’un charge l’application sur un vrai téléphone via un vrai réseau, et elle reste lente.
Cet article explique pourquoi la taille du bundle devient si souvent le mauvais objectif, d’où proviennent réellement les temps d’attente, et comment mettre en place un cycle d’optimisation qui corrige d’abord les problèmes les plus coûteux. Des bundles plus petits aident effectivement, parfois considérablement. Mais si la page est lente parce qu’elle attend des réponses du serveur, bloque le rendu, effectue des tâches inutiles, envoie trop de requêtes ou hydrate un arbre de composants volumineux, réduire encore de 20 KB ne suffira pas à la sauver.
Pourquoi la taille du bundle est devenue l’objectif de performance par défaut
La taille du bundle est attractive car c’est un chiffre. Votre processus de compilation affiche quelque chose comme ceci :
main.js 842 KB
vendor.js 611 KB
styles.css 94 KB
Quelqu’un propose de ramener la taille du JavaScript en dessous de 500 KB, et soudainement l’équipe dispose d’un objectif. On peut le contrôler dans les tests automatisés, le suivre à travers les demandes de fusion et fêter chaque réduction. Cela donne l’impression d’un progrès technique, et parfois c’en est vraiment un.
Les problèmes commencent lorsque ce chiffre cesse d’être un symptôme pour devenir l’objectif lui-même. Les équipes ont tendance à optimiser la partie de la performance qu’elles peuvent voir le plus clairement, et non celle qui coûte le plus de temps aux utilisateurs. Pour comprendre pourquoi c’est important, comparons deux applications hypothétiques.
Application A : petit bundle, tout le reste est lent
La première application distribue un bundle léger :
JavaScript: 250 KB
Pourtant tout ce qui l’entoure est coûteux en ressources :
Server response: 1.2s
Database query: 700ms
API calls before rendering: 5
Main-thread work: 900ms
Application B : bundle volumineux, accès rapide au contenu
La deuxième application inclut presque trois fois plus de JavaScript :
JavaScript: 700 KB
Cependant, son serveur et son client effectuent bien moins de travail avant que la page ne devienne utile :
Server response: 150ms
Database query: 40ms
API calls before rendering: 1
Main-thread work: 180ms
L’Application A n’est pas automatiquement gagnante. Une application de type B semblera très souvent plus rapide, car elle passe environ une seconde de moins en temps serveur, en temps de base de données, pour des requêtes séquentielles et des tâches sur le thread principal, ce qui compense largement le téléchargement supplémentaire pour la plupart des utilisateurs disposant d’une connexion convenable. Le principe fondamental à retenir pour toute discussion sur les performances est simple : les utilisateurs ne perçoivent pas les kilooctets, ils perçoivent le temps d’attente.
Le chargement de la page est un processus long, pas trois étapes
De nombreux développeurs ont en tête un modèle simplifié du chargement :
Download JavaScript
↓
Execute JavaScript
↓
Page appears
Une vraie requête traverse bien plus d’étapes, chacune pouvant provoquer des retards :
DNS
↓
Connection
↓
TLS
↓
Request
↓
Server processing
↓
Database
↓
Response
↓
HTML parsing
↓
CSS processing
↓
JavaScript download
↓
JavaScript parsing
↓
JavaScript execution
↓
Hydration
↓
API requests
↓
Rendering
↓
Layout
↓
Paint
La recherche DNS, l’établissement de la connexion et la négociation TLS ont lieu avant même que votre serveur ne voie quoi que ce soit. Le traitement par le serveur et les opérations sur la base de données suivent. Ensuite, le navigateur analyse l’HTML, traite le CSS, télécharge, analyse et exécute le JavaScript, met à jour le contenu dynamiquement, envoie des appels API, et ce n’est qu’alors qu’il affiche, organise et dessine l’interface. Lorsque l’utilisateur clique, une grande partie de ce cycle se répète.
Avec autant d’étapes, le bundle est simplement un endroit où le temps peut disparaître. « Rendre le bundle plus petit » est une mauvaise première approche, car elle suppose déjà la solution sans savoir d’où proviennent les pertes de temps.
Le serveur peut être la partie la plus lente de votre interface frontend
Les ingénieurs frontend considèrent naturellement les performances comme un problème lié au navigateur. On ouvre les outils de développement, on examine l’onglet Réseau ainsi que les blocs JavaScript, et on exécute Lighthouse. Mais une grande partie de la vitesse perçue du frontend est déterminée avant même que le navigateur ne reçoive quoi que ce soit d’utile.
Prenons une demande de tableau de bord :
GET /dashboard
Derrière cela, le serveur peut effectuer tout ceci avant de répondre :
→ authenticate user
→ fetch organization
→ fetch permissions
→ query projects
→ query project statistics
→ query notifications
→ calculate recommendations
→ render response
Si cette chaîne d’opérations prend 1,4 seconde, réduire la taille du fichier de 600 KB à 500 KB n’a que peu d’impact sur l’expérience utilisateur, car les premiers octets significatifs arrivent toujours après 1,4 seconde. Le navigateur ne peut pas afficher des données qu’il n’a pas encore reçues.
Un facteur fréquent est le code backend qui attend successivement l’exécution d’opérations indépendantes. Tout commence par la récupération des informations de l’utilisateur :
const user = await getUser();
Et cela se poursuit avec une série d’attentes supplémentaires pour l’organisation, les projets et les notifications, chacune attendant que la précédente soit terminée :
const organization = await getOrganization(user.orgId);const projects = await getProjects(organization.id);const notifications = await getNotifications(user.id);
Certaines de ces étapes dépendent réellement les unes des autres : la recherche d’organisation nécessite user.orgId, et les projets ont besoin de l’ID de l’organisation. Mais tout ce qui ne dépend pas d’un résultat antérieur subit inutilement des retards en série. Lorsque les opérations sont indépendantes, les lancer ensemble et les attendre simultanément peut considérablement réduire le temps de réponse :
const [user, notifications] = await Promise.all([
getUser(),
getNotifications()
]);
Remarquez que la version parallèle appelle getNotifications() sans identifiant d’utilisateur. Cela ne fonctionne que si les notifications peuvent être obtenues à partir de données déjà disponibles, comme la session ; sinon, cette appel doit encore attendre l’utilisateur. La règle générale consiste à cartographier le véritable graphe de dépendances et à exécuter chaque niveau de celui-ci en parallèle. Pour en savoir plus sur le choix entre ces modèles, consultez notre guide sur Promise.all, Promise.race et sequential awaits. Un tel changement peut facilement surpasser en performance tout travail de compilation.
Les méthodes en cascade coûtent plus que des simples octets
L’un des meilleurs endroits pour commencer une investigation est l’onglet Réseau, et non l’analyseur de bundles. Un modèle très courant ressemble à ceci :
HTML
↓
JavaScript
↓
API A
↓
API B
↓
API C
↓
API D
Chaque étape attend la précédente, et chaque transfert ajoute un temps de latence supplémentaire. Comparez cela à une conception où la première réponse contient déjà tout ce dont la page a besoin :
HTML
↓
API response containing everything required
La deuxième version peut transférer plus de données binaires tout en étant nettement plus rapide. Les données binaires et la latence sont deux problèmes distincts. Une réponse de 100 KB arrivant immédiatement peut surpasser une réponse de 20 KB qui nécessite quatre transferts successifs avant que la page ne puisse effectuer quoi que ce soit d’utile, surtout sur les réseaux mobiles où chaque transfert est coûteux.
Ainsi, lorsque vous constatez qu’un point de terminaison renvoie 300 KB, l’instinct est de réduire la taille des données transmises. Cela peut être utile, mais une meilleure première question est de savoir pourquoi l’utilisateur a absolument besoin de cette réponse avant de pouvoir interagir avec la page. La réponse révèle souvent des tâches qui peuvent être différées ou complètement supprimées.
La performance du JavaScript est plus importante que sa taille
Un piège similaire consiste à confondre la taille de son JavaScript avec l’effort qu’il implique. Un fichier de 500 KB n’est pas automatiquement un désastre. Ce qui compte, c’est ce que le navigateur doit en faire :
- Le télécharger.
- Le parser.
- Le compiler.
- Le exécuter.
- Créer l’état de l’application.
- Construire les arbres de composants.
- Attacher des gestionnaires d’événements.
- Hydrater le markup généré sur serveur.
- Recalculer la mise en page.
- Dessiner le résultat.
Deux applications ayant des tailles similaires peuvent présenter d’énormes différences en termes de coût d’exécution. Prenons un tableau comptant 5 000 lignes : le problème ne réside rarement dans les données elles-mêmes, mais plutôt dans le rendu des nœuds DOM correspondant à ces 5 000 lignes interactives. La solution n’est pas de réduire les 50 KB de code script, mais plutôt de ne rendre que les environ 30 lignes actuellement visibles. Cette technique, la virtualisation, permet de conserver la même application et les mêmes données tout en éliminant potentiellement la majeure partie du travail à effectuer par le navigateur.
Lorsque l’hydratation devient un goulot d’étranglement
Cette distinction est particulièrement marquée dans React et d’autres frameworks de composants qui rendent sur le serveur. Le rendu côté serveur affiche rapidement l’HTML à l’écran, mais le navigateur doit ensuite hydrater un grand arbre de composants avant que quoi que ce soit ne réagisse aux entrées utilisateur :
HTML arrives quickly
↓
User sees content
↓
Browser starts hydration
↓
Large amount of JavaScript executes
↓
Page becomes interactive
La page semble prête bien avant d’être réellement prête. C’est pourquoi ne mesurer que le moment où le contenu apparaît pour la première fois peut induire en erreur. Un tableau de bord contenant 200 composants interactifs peut générer un HTML tout à fait correct et consommer néanmoins beaucoup de ressources CPU lors du processus d’hydratation, laissant les clics sans réponse.
La question pertinente ici n’est pas de savoir si le fichier compressé est trop volumineux, mais pourquoi autant de code doit devenir interactif immédiatement. Certains composants n’ont peut-être pas besoin du JavaScript côté client du tout. Certaines interactions peuvent être isolées en petits modules indépendants. Certains widgets peuvent être chargés plus tard, et certains composants générés côté serveur n’ont peut-être jamais besoin d’hydratation. Des techniques comme celles que notre article sur le pré-rendu partiel et le rendu concurrent explore offrent bien plus que de simples discussions sur des dépendances de 30 KB.
Les scripts tiers l’emportent souvent sur votre propre code
Au préalable d’une campagne de grande envergure, vérifiez quelle partie du code que vous déploiez a été écrite par quelqu’un d’autre. Exemples typiques :
- analytique
- widgets de chat
- cartes thermiques
- tests A/B
- publicité
- Outils d’assistance client
- enregistrement des sessions
- incrustations sociales
- pixels de marketing
- Gestion du consentement
Chacun d’eux peut ajouter des requêtes, des exécutions de scripts, des modifications de mise en page et une activité réseau. Ironiquement, ces scripts font souvent l’objet d’à peine aucune vérification, tandis que les ingénieurs passent des heures à affiner le code de l’application. Une page peut charger un ensemble de ce type :
app.js
analytics.js
chat.js
tracking.js
experimentation.js
heatmap.js
L’équipe célèbre une réduction de 70 KB dans app.js, alors que la page exécute encore des centaines de kilooctets de code provenant de tiers. C’est pourquoi les budgets de performance doivent avoir une portée plus large. Plutôt que de se demander quelle est la taille du bundle, il faut se demander quel volume de code le dispositif de l’utilisateur doit traiter avant que cette page ne devienne utile. Les deux questions ont des réponses très différentes.
Les images peuvent éclipser l’intégralité du budget JavaScript
Les images de trop grande taille constituent un autre point aveugle. Une seule image principale peut avoir un impact plus important qu’un bloc JavaScript entièrement optimisé :
main.js 180 KB
hero.webp 1.4 MB
product.jpg 900 KB
background.png 2.1 MB
Si une demande de fusion supprimant une dépendance de 12 KB est acceptée, tandis qu’une image d’arrière-plan de 2,1 MB reste inchangée, l’équipe se contente de faire du théâtre en matière de priorisation au lieu d’optimiser réellement. Les images méritent le même niveau de rigueur que le code :
- Préférer les formats modernes tels que WebP ou AVIF, là où ils sont pris en charge et appropriés.
Économiser 15 KB de JavaScript ne change pas grand-chose si un téléphone doit quand même télécharger 2 MB pour une image que l’utilisateur remarque à peine.
De nombreux problèmes de performance sont des problèmes d’architecture
Plus on explore en profondeur, plus il devient évident que de nombreux problèmes de performance n’ont rien à voir avec l’optimisation du code. Ils proviennent de la structure même de l’application. Imaginez une page produit qui nécessite tout ceci :
Product
Reviews
Recommendations
Inventory
Shipping estimate
User preferences
Related products
Si chaque élément est chargé séparément après le chargement de la page, vous pouvez optimiser chaque requête et obtenir malgré tout une page lente. La meilleure approche consiste à déterminer ce que l’utilisateur doit voir en premier. La réponse initiale peut ne contenir que :
Product
Price
Availability
Primary image
Les avis, les recommandations et les produits associés peuvent être affichés par la suite. À ce stade, vous n’optimisez plus l’implémentation ; vous redéfinissez ce que signifie « prêt » pour la page, et c’est souvent là que se trouvent les plus grands gains.
Mesurer les étapes que les utilisateurs remarquent réellement
Un travail sérieux sur les performances remplace la question « quelle est la taille du paquet ? » par « quand l’utilisateur peut-il faire quelque chose d’utile ? ». Cette question permet d’obtenir de meilleures métriques.
Délai jusqu’à la première mise en page utile
Quand l’utilisateur voit-il ce pour quoi il est venu ? Cela dépend souvent de votre produit : le solde du compte, les résultats de recherche, la photo du produit.
Délai jusqu’à l’interactivité
Quand l’utilisateur peut-il interagir de manière fiable, sans que les clics ne soient absorbés par des tâches en cours ? Les versions récentes de Lighthouse ne prennent plus en compte le TTI dans leur score, mais la question sous-jacente reste pertinente pour suivre les performances de vos propres pages.
Délai de rendu du contenu principal
Quand le contenu visible principal est-il entièrement rendu ?
Délai entre l’interaction et le prochain rendu
À quelle vitesse l’interface répond-elle visuellement après une interaction de l’utilisateur ?
Décalage cumulatif du layout
Le layout change-t-il constamment tandis que l’utilisateur tente de lire ou de cliquer ?
Délai total d’obstruction
Pendant combien de temps le thread principal est-il bloqué par des tâches qui empêchent le navigateur de répondre aux entrées ?
Aucun de ces éléments ne raconte l’histoire dans son intégralité par lui-même, mais ensemble ils décrivent l’expérience bien mieux qu’une simple phrase comme celle-ci :
bundle.js = 487 KB
Une figure de paquet décrit un actif. Les métriques de performance décrivent ce que vit l’utilisateur.
Un cycle d’optimisation fiable
Lorsqu’une application lente doit être corrigée, supprimer les dépendances ne devrait pas être la première étape. Un cycle structuré est plus efficace.
1. Reproduire les conditions réalistes
Tester sur des appareils et des réseaux représentatifs, et non seulement sur un ordinateur portable rapide connecté au Wi-Fi du bureau. Beaucoup de vos utilisateurs n’ont ni l’un ni l’autre.
2. Mesurer pour identifier le point lent
Déterminer où passe le temps. Le problème principal est-il l’un de ceux-ci ?
server response?
network?
rendering?
JavaScript execution?
layout?
images?
third-party scripts?
3. Identifier le coût dominant
Évitez de corriger cinq choses en même temps. Trouvez le facteur ayant le plus grand impact.
4. Changer une chose
Apportez la plus petite modification architecturale ou d’implémentation possible pour résoudre ce goulot d’étranglement, afin de pouvoir attribuer les résultats obtenus.
5. Mesurer à nouveau
Si l’amélioration n’apparaît pas dans les chiffres, ne supposez pas pour autant qu’elle ait fonctionné.
6. Consolider les gains grâce à une vérification de régression
Les améliorations s’effacent rapidement : quelqu’un ajoute une dépendance, une équipe de développement intègre un widget, un composant devient plus gourmand en ressources, une requête passe à un traitement séquentiel, et trois mois plus tard vous êtes revenu au point de départ. Les performances nécessitent des mécanismes de contrôle automatisés dans les processus CI et les systèmes de surveillance, et non des nettoyages occasionnels et pénibles.
La taille du bundle reste importante, à sa place
Rien de tout cela ne rend la taille du bundle insignifiante. Les bundles volumineux augmentent les coûts de téléchargement, d’analyse, de compilation et d’exécution, et les conséquences sont particulièrement sévères sur les appareils et réseaux lents. Le splitage du code, le tree shaking, le chargement différé et la suppression des dépendances inutilisées sont tous utiles. Il s’agit de les appliquer lorsque les données montrent qu’ils représentent le plus gros problème.
Une évaluation des performances bien menée, en fonction de l’impact, pourrait se dérouler comme suit. Tout d’abord, une réponse du serveur lente :
Problem:
900ms server response
Résolu en exécutant les requêtes backend en parallèle :
Action:
parallelize backend requestsResult:
-420ms
Ensuite, un chargement coûteux du tableau de bord :
Problem:
large dashboard hydration
Résolu en différant le chargement des composants qui n’ont pas besoin d’être interactifs immédiatement :
Action:
defer non-critical interactive componentsResult:
-280ms main-thread work
Puis, une image principale trop grande :
Problem:
hero image is 1.8 MB
Résolu grâce à une livraison adaptative dans des formats modernes :
Action:
responsive WebP/AVIF deliveryResult:
-1.2 MB transferred
C’est seulement après tout cela qu’une dépendance JavaScript lourde arrive en tête de la liste :
Problem:
large JavaScript dependency
La remplacer permet néanmoins d’économiser une quantité significative :
Action:
replace dependencyResult:
-60 KB
Cette correction reste un bon travail. Elle devrait simplement figurer en quatrième position dans la file, et non en première.
Points clés
- Optimisez en fonction du temps d’attente, et non pour le plus petit fichier possible. La taille du bundle n’est qu’un des nombreux indicateurs.
- Examinez le serveur et la chaîne de requêtes avant d’utiliser un analyseur de bundles ; la latence et les allers-retours coûtent souvent plus que le nombre de octets.
- Évaluez JavaScript en fonction du travail qu’il génère : analyse, exécution, rendu et hydratation, et non seulement sa taille.
- Faites un audit des scripts et images tiers avec la même rigueur que pour votre propre code.
- Rédefinissez ce qu’est être « prêt » pour chaque page afin que le contenu essentiel arrive en premier, suivi du reste.
Lectures complémentaires
- Ce qui fait vraiment de los desarrollpeurs frontend des personnes valables à l’ère de l’IA — Explique pourquoi la compréhension, le jugement et la réflexion au niveau du système sont désormais plus importants que la maîtrise des frameworks, à mesure que l’IA prend le relais dans le codage frontend de routine.
- Au-delà du P95 : Mesurer la latence réellement vécue par les utilisateurs — Pourquoi un P95 correct peut coexister avec un produit lent, comment le temps d’attente dans les files et les effets de propagation échappent aux tableaux de bord, et comment le suivi du temps par étape met fin aux accusations concernant la latence.
- Ce que JSON.stringify omet en silence, transforme et refuse de serialiser — Découvrez quels valeurs JavaScript JSON.stringify supprime ou modifie, comment toJSON, les remplaçants et les réanimateurs y remédient, et quand structuredClone est l’outil le plus adapté.
- Où une fonction est écrite détermine ce qu’elle voit : le champ lexical de JavaScript — Comprenez comment JavaScript résout un nom de variable à travers les environnements lexicaux, pourquoi le point d’appel n’a jamais d’importance pour la recherche, et comment cela se reflète dans les gestionnaires de React.
- Ce que React Compiler optimise et ce qu’il laisse à gérer — Comprendre quelles tâches de performance React Compiler automatise, pourquoi les API lentes et les fichiers volumineux restent votre responsabilité, ainsi que comment l’intégrer en toute sécurité dans une base de code React existante.
- Démystification du diffing du Virtual DOM : ce que compare React et pourquoi cela a un effet — Comprendre ce qu’est réellement le Virtual DOM de React, comment la réconciliation compare deux arbres d’éléments, quels changements ont lieu lors de la phase d’application et d’où proviennent les avantages en termes de performance.
- Reflow, Repaint, Composite : quel est le coût pour le navigateur de chaque modification CSS — Suivez le parcours d’HTML et CSS à travers DOM, CSSOM, le mise en page, la peinture et la composition, et découvrez pourquoi les changements de largeur coûtent plus cher que les changements de couleur ainsi que comment éviter les problèmes de mise en page.