Performance frontend en entreprise : des budgets, pas des exploits
Performance des apps d'entreprise : des vrais utilisateurs, budgets de qui bloquent la CI, routes lazy, longues listes, images.

Une application d'entreprise devient rarement lente en une seule version. Elle ralentit par petites touches : une dépendance, puis un widget, puis un tableau « temporaire », et personne ne répond du total. Puis un employé d'agence, sur un vieux PC, attend l'écran des opérations, et la correction devient un projet.
Dans une base de code qui vit longtemps, la performance n'est pas un héroïque. C'est un ensemble de limites que le vérifie chaque jour, et un moyen de voir ce que ressentent les vrais utilisateurs.
Les quelques concepts qui comptent
Les Core Web Vitals, mesurés aussi chez les utilisateurs lents. Les Core Web Vitals sont trois chiffres qui disent comment une page est ressentie. (Largest Contentful Paint) : la vitesse d'affichage du contenu principal, bonne à 2,5 secondes ou moins. (Interaction to Next Paint) : la vitesse de réaction de la page à un clic, bonne à 200 millisecondes ou moins. (Cumulative Layout Shift) : à quel point la page bouge pendant le chargement, bon à 0,1 ou moins. web.dev conseille de mesurer « le 75e centile des chargements de page, séparément sur mobile et sur ordinateur ». Le , c'est ceci : on trie tous les chargements de page du plus rapide au plus lent et on prend celui qui se trouve aux trois quarts. S'il est bon, trois utilisateurs sur quatre ont une bonne page. Une moyenne cacherait le quart le plus lent ; le centile, non.
Image : tt-tech.io, adapté de web.dev, Web Vitals, CC BY 4.0.
L'INP pour les écrans chargés de données. L'INP « évalue la réactivité globale d'une page aux interactions de l'utilisateur ». Une app de back-office se charge une fois le matin et reçoit des clics toute la journée : filtres, tri, ouverture d'une ligne. C'est là que les utilisateurs en entreprise sentent la lenteur, donc surveillez d'abord l'INP sur ces écrans.
Des budgets dans le build. Un est « un ensemble de limites imposées aux métriques qui influencent la performance d'un site ». les intègre : une section budgets dans la configuration de build, avec maximumWarning et maximumError par type. Par défaut, le type initial avertit à 500 ko et échoue à 1 Mo. Un budget dans un wiki s'érode. Un budget dans le build est la même règle pour toutes les équipes.
Le par route et par widget. Une route lazy par fonctionnalité garde le code de chaque équipe hors du premier chargement des autres. À l'intérieur d'un écran, les blocs @defer « réduisent la taille du bundle initial » en reportant le code « qui n'est pas strictement nécessaire au rendu initial d'une page ». React fait la même chose avec lazy et un fallback <Suspense>.
Les longues listes et les tableaux. Le <cdk-virtual-scroll-viewport> du affiche de longues listes « en ne rendant que les éléments qui tiennent à l'écran ». Pour une liste de dix mille opérations, demandez-vous d'abord si le serveur doit paginer. Le règle le rendu ; il ne règle pas le téléchargement de dix mille lignes.
Le coût du rendu. Avec et par défaut dans les versions récentes d'Angular, une grande partie de l'ancien coût de la détection des changements a disparu. Ce qui reste, ce sont les listes : track permet à Angular d'exécuter « le minimum d'opérations DOM nécessaires quand les données changent », et un suivi par référence d'objet « peut ralentir nettement les mises à jour du rendu ».
Les images dans les apps qui en affichent beaucoup. Quand un écran montre beaucoup d'images, photos de produits, documents scannés, avatars, utilisez la directive d'image d'Angular : <img ngSrc> au lieu de <img src>. NgOptimizedImage « facilite l'adoption des bonnes pratiques de performance pour le chargement des images » : les images non marquées priority reçoivent loading=lazy par défaut, la directive « génère un srcset adapté » à partir de l'attribut sizes, et elle exige une largeur et une hauteur, ce qui évite les décalages de mise en page. Marquez l'image principale de la page en priority, car c'est en général l'élément LCP.
Des sur de vrais appareils. Les viennent d'« un environnement contrôlé, avec des réglages d'appareil et de réseau prédéfinis » ; les données terrain viennent de vrais utilisateurs, elles reflètent donc « les appareils et les conditions réseau réels ». Votre ordinateur portable n'est ni le PC de l'agence ni le vieux téléphone Android de vos clients.
L'ordinateur portable du développeur est le seul appareil que vos utilisateurs n'ont jamais.
Quand choisir quoi
- Type de budget : commencez par
initialetanyComponentStyle. Ajoutez des budgets par bundle quand une fonctionnalité lazy commence à grossir. @deferou route lazy : une route lazy quand le code appartient à un autre écran ;@deferpour un widget lourd sous la ligne de flottaison du même écran (un graphique, un éditeur).- Défilement virtuel ou pagination : la pagination côté serveur quand les utilisateurs cherchent et filtrent ; le défilement virtuel quand ils parcourent vraiment une longue liste sur une seule page.
- Laboratoire ou terrain : dans la CI pour détecter tôt les régressions, les données terrain pour décider quoi corriger.
Sur Atlas Bank
Atlas Bank charge chaque fonctionnalité en lazy loading. apps/atlas/src/app/app.routes.ts a un loadChildren par espace, le tableau de bord, les comptes, les demandes et les deux coins , donc l'écran ne coûte quelque chose que lorsqu'un utilisateur l'ouvre.
Les budgets de apps/atlas/project.json sont les valeurs par défaut de la CLI, gardées exprès :
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kb",
"maximumError": "8kb"
}
],Le build utilise la configuration de production par défaut, et la CI du dépôt lance build sur chaque , donc le budget est vérifié avant la fusion. Les listes utilisent @for avec track t.id, par exemple le tableau des opérations dans libs/feature/accounts/src/lib/account-detail.ts.
Ce qu'Atlas n'a pas encore : une longue liste d'opérations. Ce serait un petit ajout pour un exercice de défilement virtuel ; aujourd'hui, les données de démo comptent quinze opérations.
Ce que j'ai laissé de côté, et pourquoi
- , , compression. Le build s'en charge.
- . Un vrai outil, mais dans les apps d'entreprise, le travail lourd a plus souvent sa place sur le serveur que dans un worker.
- , , . Une ligne : utilisez-les quand une mesure montre qu'une ressource arrive tard, pas par défaut.
- Formats d'image et d'images.
NgOptimizedImagefonctionne avec un loader d'images quand vous avez un CDN ; le choix des formats relève de l'exploitation.
Un budget qui fait échouer le build protège l'app contre toutes les équipes, y compris la vôtre.
Dès lundi
- Ouvrez votre
angular.jsonou votreproject.json. S'il n'y a pas de sectionbudgets, ou si la limite d'erreur est très au-dessus de la taille actuelle, fixez-la juste au-dessus du bundle initial d'aujourd'hui. - Prenez l'écran sur lequel vos utilisateurs cliquent le plus et regardez son INP dans les données terrain, si vous les collectez. Sinon, c'est le premier .
- Trouvez le plus gros
@forou.map()de l'app et vérifiez quelle clé il utilise pour suivre ses éléments.
Pour votre agent de code
Les règles de cet article, écrites pour un agent. Collez-les dans votre AGENTS.md, enregistrez-les comme , ou indiquez le fichier à votre agent.
# Frontend performance rules for enterprise apps Source: https://tt-tech.io/blog/frontend-performance-enterprise-budgets For Angular and React frontends. Add to AGENTS.md or save as a skill. Your project's own rules win where they differ. ## Rules 1. Keep a `budgets` section in the build configuration (`angular.json` or `project.json`) with `maximumWarning` and `maximumError`. Start with `initial` and `anyComponentStyle`. Add per-bundle budgets when a lazy feature starts to grow. 2. When there is no budget, or the error limit is far above today's size, set it just above the current initial bundle. 3. Load each feature through its own lazy route. Code that belongs to another screen goes in a lazy route; a heavy widget below the fold on the same screen (a chart, an editor) goes in `@defer`. In React, use `lazy` with a `<Suspense>` fallback. 4. Track every `@for` by a stable id (`track item.id`), never by object reference. Give React lists rendered with `.map()` a stable key in the same way. 5. For long lists, paginate on the server when users search and filter. Use virtual scroll (`<cdk-virtual-scroll-viewport>`) when they really scan a long list on one page. Virtual scroll fixes rendering, not downloading. 6. In image-heavy Angular screens, use `NgOptimizedImage` (`<img ngSrc>`, not `<img src>`) with a width and height, and `sizes` for the `srcset`. Mark the main image of the page `priority`. 7. Judge Core Web Vitals at the 75th percentile of page loads, mobile and desktop apart, never by average. Good is LCP within 2.5 seconds, INP at 200 milliseconds or less, CLS at 0.1 or less. 8. On data-heavy screens (filters, sorting, opening a row), watch INP first. 9. Use lab data (Lighthouse in CI) to catch regressions and field data from real users to decide what to fix. A run on a developer machine is not what users get. 10. Add `preload`, `prefetch` or `preconnect` only when a measurement shows a resource arrives late, not by default. ## Before you say done - Run the production build. It must pass the budgets. Report any new budget warning and the initial bundle size before and after. - Search the diff for `@for` blocks that track by object or `$index`, and for `.map()` lists without a stable key. - Search the diff for `<img src` in Angular templates, and for heavy libraries imported outside a lazy route or `@defer`.
Sources
- web.dev, Web Vitals
- web.dev, Interaction to Next Paint (INP)
- web.dev, Performance budgets 101
- Angular, Building Angular apps
- Angular, Deferred loading with @defer
- React, lazy
- Angular CDK, Scrolling
- Angular, Control flow
- Angular, Optimizing images with NgOptimizedImage
- web.dev, Why lab and field data can be different (and what to do about it)