Accueil / Articles

Angular plus rapide en faisant moins : dix techniques au-delà de OnPush et trackBy

Réduire les pertes réseau, les opérations réactives, DOM et CPU dans Angular moderne grâce à @defer, à l’hydratation incrémentale, à resource(), aux signaux calculés, aux phases de rendu et aux Web Workers.

Éditeur : REACTAPP.TOP16 min de lecture3569 mots

Sources: How to Make Angular Extremely Fast: 10 Performance Tricks Most Developers Miss

Le temps de exécution d’Angular est rarement la cause pour laquelle une application Angular semble lente. Bien plus souvent, l’application télécharge, initialise, recalcule et affiche des éléments que personne n’a demandés. Les solutions habituelles, telles que OnPush, les routes différées, trackBy, la compilation AOT, les builds de production et un nombre réduit d’appels HTTP, restent utiles, mais les versions récentes d’Angular offrent un contrôle bien plus fin : quand le code est téléchargé, quand les composants deviennent interactifs, quand les requêtes sont annulées, quel état rend les données invalides, quand les opérations sur le DOM s’exécutent et sur quel thread se déroulent les calculs lourds. Ce guide présente dix techniques basées sur ce contrôle, explique comment les combiner et dans quel ordre les appliquer une fois que vous avez identifié où passe réellement votre temps.

L’idée principale est simple : au lieu de rendre Angular plus rapide pour des tâches inutiles, organisez les choses de manière à ce que ces tâches ne se produisent jamais.

Où passe le temps d’une application Angular

Angular Application
                           │
          ┌────────────────┼────────────────┐
          │                │                │
       Network           CPU              DOM
          │                │                │
     JS downloads      Change detection   Layout
     API requests      Computation        Paint
     Dependencies      Parsing            Reflow
          │                │                │
          └────────────────┼────────────────┘
                           │
                    User Experience

Ces coûts interagissent. Ajuster la détection de changements ne sert à peu de chose si le premier chargement de la page télécharge 5 MB de JavaScript, et un petit bundle ne vous aide pas non plus si chaque interaction déclenche un calcul coûteux. La séquence à viser est : télécharger moins, initialiser moins, calculer moins, afficher moins et bloquer moins le thread principal. Les dix techniques ci-dessous visent chacune une ou plusieurs de ces étapes.

1. Faire en sorte que @defer génère des blocs véritablement distincts

Le bloc @defer est l’un des moyens les plus simples de réduire la quantité de JavaScript nécessaire au démarrage. Imaginez un tableau de bord avec des graphiques, des analyses, des rapports, des cartes, des lecteurs vidéo et des widgets administratifs. Peu d’entre eux doivent être présents dès l’ouverture de la page, pourtant un modèle standard les charge tous immédiatement :

<app-heavy-chart />
<app-map />
<app-report />

En enveloppant un composant lourd dans un bloc différé, on indique à Angular de télécharger son code uniquement lorsque un déclencheur est activé, ici lorsque le placeholder entre dans la zone de visualisation :

@defer (on viewport) {
  <app-heavy-chart />
}

Éviter d’inclure les composants différés dans les imports en bloc

Il existe un piège subtil : @defer ne peut séparer que le code qui n’est pas déjà accessible via un import immédiat. Si le composant hôte importe le composant différé à travers un fichier en bloc, ce dernier entraîne avec lui tous les autres éléments exportés ainsi que leurs dépendances dans le même graphe. Ainsi, au lieu d’importer depuis un fichier index :

import { HeavyComponent } from './index';

Importer le composant depuis son propre fichier :

import { HeavyComponent } from './heavy.component';

L’objectif est de maintenir le graphe des dépendances derrière chaque bloc différé aussi isolé que possible. La première configuration à éviter est indiquée comme suit :

BAD

Et elle ressemble à ceci, avec un seul lien reliant tous les widgets au tableau de bord :

Dashboard
   │
   └── index.ts
        ├── Chart
        ├── Map
        ├── Reports
        ├── Video
        └── Analytics

La configuration préférée est indiquée comme suit :

GOOD

Et elle permet à le graphique gourmand d’être importé directement, ce qui lui donne ainsi son propre bloc :

Dashboard
   │
   └── HeavyChart
          │
          └── heavy-chart.chunk.js

Les imports via lien sont l’une des raisons cachées les plus fréquentes pour lesquelles un bloc @defer échoue à réduire la taille du bundle initial. La manière la plus simple de vérifier le succès est d’examiner les résultats de la compilation à la recherche d’un nouveau bloc différé après avoir ajouté le bloc en question.

Les avantages s’accumulent : un ensemble initial plus petit signifie moins de données à télécharger, à analyser, à compiler et à exécuter, et toutes ces économies interviennent avant même que Angular ne rende quoi que ce soit.

2. Hydrater uniquement ce dont les utilisateurs ont besoin en premier

Avec le rendu côté serveur, un deuxième goulot d’étranglement apparaît une fois l’HTML reçu. Le serveur peut générer le markup rapidement, mais le navigateur doit ensuite rendre toute la page interactive, ce qui implique de télécharger et d’exécuter le code de chaque composant qu’elle contient. Prenons un layout de tableau de bord typique :

Dashboard
│
├── Header
├── Navigation
├── KPI Cards
├── Huge Chart
├── Map
├── Analytics Table
├── Video Player
└── Recommendations

Toutes ces sections n’ont pas besoin de répondre immédiatement aux entrées. L’hydratation complète classique traite la page comme une seule unité :

SSR HTML
   ↓
Hydrate EVERYTHING
   ↓
Interactive

L’hydratation incrémentale permet de diviser ce processus en étapes, de sorte que la page devient visible immédiatement et que les différentes sections deviennent progressivement interactives :

SSR HTML
   ↓
Show page immediately
   ↓
Hydrate only what is needed
   ↓
Hydrate other sections later

Cela s’exprime avec les mêmes blocs @defer, complétés par des déclencheurs d’hydratation. Dans l’exemple suivant, le serveur génère toujours l’HTML du graphique, le client l’hydrate lorsque le navigateur est inactif, et dans un rendu purement côté client, le bloc se charge lors d’une interaction :

@defer (on interaction; hydrate on idle) {
  <app-heavy-chart />
}

L’avantage architectural est que le rendu de l’HTML et sa mise en interactivité ne forment plus une étape unique où tout doit fonctionner ou rien. L’hydratation incrémentale doit être activée dans votre configuration d’hydratation ; consultez le guide SSR correspondant à votre version d’Angular pour connaître l’option de fournisseur exacte.

3. Annuler les requêtes obsolètes avec resource()

La recherche en temps réel est un cas classique de gaspillage des ressources réseau. Au fur et à mesure que l’utilisateur tape, la saisie passe par toutes les valeurs intermédiaires :

a
an
ang
angu
angul
angula
angular

Une implémentation naïve peut envoyer une requête pour chacune d’elles :

Request 1 → "a"
Request 2 → "an"
Request 3 → "ang"
Request 4 → "angu"
Request 5 → "angul"
Request 6 → "angular"

Seule la dernière réponse compte, les cinq autres étant donc inutiles. Angular moderne propose resource() et httpResource() pour un chargement asynchrone réactif. On commence par un signal contenant la requête :

const query = signal('');

Ensuite, on déclare une ressource dont la fonction params lit ce signal et dont le loader effectue la requête. Dans l’exemple ci-dessous, le loader est placé sur la même ligne que la parenthèse de fermeture de params ; il s’agit de deux propriétés distinctes de l’objet des options :

const results = resource({
  params: () => ({
    q: query()
  }),  loader: ({ params, abortSignal }) =>
    fetch(
      `/api/search?q=${params.q}`,
      { signal: abortSignal }
    ).then(res => res.json())
});

Chaque fois que les paramètres changent alors qu’un chargement est encore en cours, Angular annule le précédent. Cela fonctionne uniquement parce que le loader transmet le signal fourni à fetch, c’est là qu’il faut prêter attention :

{ signal: abortSignal }

Si on l’omet, la demande précédente continue de s’exécuter jusqu’à son terme. Deux améliorations sont à apporter dans du code réel : envelopper la requête dans encodeURIComponent avant de l’insérer dans l’URL, et se rappeler que l’annulation n’est pas un mécanisme de débouncing. Chaque frappe au clavier déclenche toujours une demande qui est ensuite interrompue ; il convient donc d’associer cette ressource à un signal de requête débounced si l’on souhaite également éviter d’envoyer ces demandes. La demande la plus rapide est celle que l’on arrête dès qu’elle devient irrelevante, et ce principe s’avère utile pour les recherches, l’autocomplétion, le filtrage, les tableaux de bord en temps réel, ainsi que pour les IDs qui changent rapidement ou les séquences d’appels dépendants.

4. Garder les signaux petits et ciblés

Les signaux sont efficaces, mais ils peuvent encore être utilisés d’une manière qui annule cette efficacité. Un schéma courant consiste à faire en sorte qu’un seul signal contienne l’état de toute une page :

const pageState = signal({
  user: {},
  filters: {},
  notifications: [],
  products: [],
  statistics: {},
  settings: {}
});

Si seuls les filtres changent, tous les consommateurs de pageState sont notifiés, car la valeur du signal dans son ensemble a été remplacée. Diviser les éléments d’état indépendants en signaux distincts réduit l’impact. Le premier devient :

const user = signal(...);

et les autres suivent le même schéma (ici affichés ensemble sur une seule ligne ; dans du code réel, chacun occupe sa propre ligne) :

const filters = signal(...);const notifications = signal(...);const products = signal(...);const statistics = signal(...);const settings = signal(...);

Désormais, chaque consommateur ne dépend que de l’état qu’il lit réellement. Un tableau est un bon modèle mental. Lorsqu’une seule cellule change :

A1 changes

on ne souhaite pas que tout le tableau soit recalculé, seulement la chaîne qui en dépend :

A1
 ↓
Dependent cells
 ↓
Only affected UI

Un signal d’objet volumineux rend obsolètes les consommateurs qui ne s’intéressent qu’à une partie de cet objet ; les signaux de petite taille permettent des domaines d’invalidation plus restreints. Plutôt que de se demander si tout l’état peut tenir dans un seul signal, demandez ce qui doit réellement réagir lorsque une valeur particulière change. Si vous avez vraiment besoin d’un objet composite, dérivez-le à l’aide de computed() à partir des signaux de petite taille plutôt que de le stocker comme source unique de vérité.

5. Éloigner les calculs coûteux des templates

Appeler une fonction depuis un template est l’un des moyens les plus simples de créer du travail redondant :

<div>
  {{ calculateTotal(items()) }}
</div>

Cela semble inoffensif, mais Angular réévalue cette expression à chaque fois que la vue est vérifiée, ce qui fait que le coûteux calculateTotal() s’exécute encore et encore, même lorsque items n’a pas changé. La solution consiste à utiliser un signal computed(). Déclarez l’état source :

const items = signal<Item[]>([]);

Déduire la valeur totale à partir de cela :

const total = computed(() =>
  items().reduce(
    (sum, item) => sum + item.price,
    0
  )
);

et lire le signal dérivé dans le template :

<div>
  {{ total() }}
</div>

computed() mémorise son résultat et le recalcule uniquement lorsque le signal qu’il lit a changé, plutôt que lors de chaque évaluation du template. Cela en fait un outil idéal pour le filtrage, le tri, l’agrégation, les statistiques, les indicateurs de tableau de bord et tout autre état d’interface dérivé. Une liste de produits filtrés suit la même structure, en partant d’un signal de terme de recherche :

const searchTerm = signal('');

et une liste dérivée qui dépend à la fois des produits et du terme :

const filteredProducts = computed(() =>
  products().filter(product =>
    product.name.includes(searchTerm())
  )
);

Cela remplace les appels de template tels que ceux-ci, en transférant le traitement au graphe réactif :

{{ filterProducts(products(), searchTerm()) }}

Un détail : includes est sensible à la casse, donc une recherche réelle normaliserait généralement d’abord les deux chaînes. Considérez computed() comme un cache pour l’état dérivé qui se met correctement à jour.

6. Chargez en différé les services lourds, pas seulement les composants

La charge en différé est généralement abordée à deux niveaux :

Lazy route
Lazy component

Cependant, les services peuvent eux-mêmes faire appel à de grands arbres de dépendances. Un service de prévisualisation Markdown en est un bon exemple :

MarkdownService
   │
   ├── marked
   ├── highlight.js
   ├── syntax definitions
   └── other dependencies

Si le service n’est utilisé que lorsque quelqu’un clique sur le bouton « Prévisualiser Markdown », il n’y a aucune raison d’inclure le parseur, l’highlighteur de syntaxe et leurs grammaires dans le bundle initial. La technique consiste à résoudre le service via une importation dynamique au moment où il est nécessaire. Conceptuellement, cela se présente ainsi, en utilisant un outil d’aide injectAsync() :

const markdownService = injectAsync(
  () =>
    import('../markdown.service')
      .then(m => m.MarkdownService)
);

Le composant attend ensuite le service nécessaire à l’intérieur du gestionnaire :

async preview(content: string) {
  const service = await this.markdownService();

et l’utilise pour afficher le contenu :

  this.html = service.render(content);
}

Considérez injectAsync() comme un pattern plutôt que comme une API centrale garantie : vérifiez si votre version d’Angular ou une bibliothèque que vous utilisez la propose. Le même effet peut être obtenu avec un simple await import('../markdown.service') à l’intérieur du gestionnaire, suivi de la résolution de la classe via l’injecteur. Dans les deux cas, le service et ses dépendances lourdes se trouvent dans leur propre chunk. La règle générale est de charger les dépendances coûteuses là où elles sont utiles, et non seulement aux limites des routes.

7. Considérer les lectures et écritures du DOM comme une API coûteuse

Parfois, le goulot d’étranglement n’est pas du tout JavaScript mais le moteur de mise en page du navigateur. Cela se produit lorsque le code mélange des opérations de mesure et des modifications :

read DOM
write DOM
read DOM
write DOM
read DOM
write DOM

Chaque lecture effectuée après une écriture peut forcer le navigateur à recalculer le layout de manière synchrone, un phénomène connu sous le nom de « layout thrashing ». Angular propose des hooks sensibles au rendu pour cela. Le premier est :

afterNextRender()

et le second, qui prend en charge des phases explicites, est :

afterRenderEffect()

Avec afterRenderEffect(), vous placez les mesures dans la phase earlyRead :

afterRenderEffect({
  earlyRead: () => {
    return element.getBoundingClientRect();
  },

et les mutations dans la phase write, qui reçoit la valeur retournée par la lecture (ce fragment fait partie de la même appel TypeScript, et non du CSS) :

  write: height => {
    element.style.height = `${height}px`;
  }
});

Angular exécute les phases de lecture de tous les composants avant leurs phases d’écriture, de sorte que le navigateur perçoit le travail regroupé de cette manière :

READ
 ↓
READ
 ↓
READ
 ↓
WRITE
 ↓
WRITE
 ↓
WRITE

au lieu d’alterner constamment :

READ → WRITE → READ → WRITE

Cette coordination basée sur les phases est particulièrement importante pour les graphiques, les cartes, les animations, la gestion du redimensionnement et du défilement, les bibliothèques d’interface utilisateur tierces ainsi que les tableaux de bord complexes, où de nombreux composants modifient le DOM après chaque rendu.

8. Détection des changements en lot avec fusion des événements et des exécutions

Dans les applications basées sur des zones, plusieurs déclencheurs peuvent être activés rapidement consécutivement pour ce qui correspond logiquement à une seule action de l’utilisateur :

click
 ↓
event bubbles
 ↓
parent handler
 ↓
ngZone.run()
 ↓
another update

En l’absence de fusion, chaque étape peut planifier sa propre passe de détection des changements. Angular peut les fusionner lorsque vous activez ces deux options dans provideZoneChangeDetection:

bootstrapApplication(AppComponent, {
  providers: [
    provideZoneChangeDetection({
      eventCoalescing: true,
      runCoalescing: true
    })
  ]
});

Au lieu d’une passe par événement :

Event A → Change Detection
Event B → Change Detection
Event C → Change Detection

les tâches superposées se résument en une seule passe :

Event A ─┐
Event B ─┼──→ One Change Detection
Event C ─┘

Cela est particulièrement utile dans les applications ayant de nombreux gestionnaires d’événements, des flux WebSocket très actifs, des appels fréquents à ngZone.run() ou des tableaux de bord hautement interactifs. Gardez à l’esprit que ces options s’appliquent à la détection de changements basée sur Zone.js ; les versions récentes d’Angular privilégient une détection de changements sans zone, où le planification est déjà gérée par des signaux et des notifications explicites. Vérifiez donc quel mode votre application utilise avant d’y recourir.

9. Identifier les dépendances que vous n’aviez pas l’intention d’inclure

Certains des plus gros problèmes de performance ne sont pas liés à Angular mais aux importations. Vous pouvez avoir besoin d’une seule fonction provenant d’une bibliothèque, mais votre structure d’importation peut en inclure bien plus :

Application
   │
   └── Library
        │
        ├── Feature A
        ├── Feature B
        ├── Feature C
        ├── Feature D
        └── Feature E

alors que tout ce que vous avez réellement utilisé était :

Feature A

C’est pourquoi l’analyse des bundles fait partie intégrante de toute enquête sur les performances. Générez des statistiques sur la version de production :

ng build --configuration=production --stats-json

Ensuite, examinez les résultats pour voir quels fichiers et bibliothèques dominent le chargement. Des outils tels que source-map-explorer fonctionnent à partir de cartes sources, il faut donc les activer dans le processus de compilation que vous analysez ; le format du fichier statistique dépend du générateur utilisé par votre projet, il convient donc de vérifier quel analyseur il prend en charge. La question importante n’est pas de savoir si l’application semble lente, mais plutôt ce que vous envoyez exactement au navigateur. Une fois cela visible, la plupart des optimisations deviennent évidentes. Pour éviter les dérives ultérieures, envisagez l’application de budgets de performance dans CI.

10. Déplacer les calculs lourds hors du thread principal

Parfois, le framework est en ordre et c’est l’algorithme qui pose problème. Supposons qu’un tableau de bord reçoive un ensemble de données de cette taille :

100,000 records

et exécute des statistiques, du traitement d’images, de la géométrie, de grandes transformations, ainsi que des analyses ou agrégations complexes dessus dans le navigateur. Sur le thread principal, ce travail concurrence avec tout le reste :

Main Thread
│
├── Angular
├── Rendering
├── User input
├── Animations
└── 100K-record calculation

Le résultat est un fonctionnement instable : entrée figée, animations saccadées et rendu retardé. Un Web Worker déplace les calculs sur un thread séparé et n’envoie en retour que le résultat :

Main Thread                  Worker
     │                          │
     │──── large dataset ─────>│
     │                          │
     │                       Calculate
     │                       Transform
     │                       Analyze
     │                          │
     │<──── result ─────────────│
     │
Render UI

Angular CLI peut créer une structure de base pour un worker et configurer ses paramètres :

ng generate web-worker app

et le composant le crée à l’aide d’une URL relative au module afin que le bundler puisse le trouver et le compiler :

const worker =
  new Worker(
    new URL('./app.worker', import.meta.url)
  );

Un travailleur ne rend pas le travail propre à Angular moins coûteux. Ce qu’il fait, c’est empêcher que la logique d’application coûteuse bloque le rendu et les interactions de l’utilisateur. Utilisez les travailleurs uniquement lorsque le calcul est réellement lourd, car les données transmises entre threads sont copiées par clonage structuré, et ce transfert a lui aussi un coût. Pour une analyse neutre par rapport au framework, consultez quand déplacer le travail du CPU vers un Web Worker.

Combinaison des techniques sur une même interface de tableau de bord

Ce ne sont pas dix astuces isolées ; elles se renforcent mutuellement. Une interface de tableau de bord d’analyse non optimisée effectue tout en amont :

Browser
  │
  ├── Download huge JS bundle
  ├── Initialize everything
  ├── Hydrate everything
  ├── Start every API request
  ├── Recalculate large state
  ├── Execute expensive template functions
  ├── Perform DOM reads/writes
  └── Calculate large datasets on main thread

Avec l’application de ces techniques, chaque couche ne fait que ce que nécessite le moment présent :

Browser
  │
  ├── Smaller initial bundle
  │      └── @defer
  │
  ├── Partial interactivity
  │      └── Incremental hydration
  │
  ├── Only latest API request
  │      └── resource/httpResource
  │
  ├── Narrow reactive updates
  │      └── Fine-grained signals
  │
  ├── Cached derived values
  │      └── computed()
  │
  ├── Heavy services loaded later
  │      └── injectAsync()
  │
  ├── Coordinated DOM work
  │      └── afterRenderEffect()
  │
  ├── Fewer redundant CD cycles
  │      └── Event coalescing
  │
  ├── Smaller dependencies
  │      └── Bundle analysis
  │
  └── Heavy computation elsewhere
         └── Web Worker

L’application ne fonctionne pas vraiment plus rapidement, mais effectue beaucoup moins de travail.

Une formule concise pour la performance

On peut résumer cette approche comme une somme de réductions :

Performance
    =
Less JavaScript
+
Less Network Work
+
Less Reactive Work
+
Less DOM Work
+
Less Main-Thread Work

En termes encore plus succincts : un Angular rapide signifie effectuer moins de travail, le faire plus tard et ne le faire que lorsque c’est nécessaire.

Un ordre pratique d’optimisation

N’appliquez pas tout en même temps. Mesurez d’abord, puis procédez par étapes dans cet ordre approximatif, car les premières actions ont généralement le plus grand impact sur le démarrage.

Tout d’abord, réduisez le JavaScript initial :

@defer
injectAsync()
bundle analysis

Ensuite, réduisez le travail de hydration initial :

Incremental Hydration

En troisième lieu, éliminez le travail réseau inutile :

resource()
httpResource()

En quatrième lieu, restreignez l’invalidation réactive :

Fine-grained signals
computed()

En cinquième lieu, réduisez la charge de rendu :

afterRenderEffect()
afterNextRender()

Sixièmement, supprimer la détection redondante des modifications :

eventCoalescing
runCoalescing

Septièmement, éliminer les goulots d’étranglement du processeur :

Web Workers

Mesurer avant d’optimiser

La règle la plus importante est d’éviter d’optimiser à l’aveugle. L’application systématique de cette approche n’est pas une stratégie valable :

OnPush

De même, il ne faut pas introduire ces solutions sans connaître l’origine du goulot d’étranglement :

memoization
workers
RxJS operators
defer blocks

Utilisez Chrome DevTools, Lighthouse, Angular DevTools, l’analyse des fichiers compilés en production ainsi que le panneau de performance du navigateur pour effectuer un profilage. Ensuite, posez-vous la question fondamentale :

What is actually slow?

Et restreignez la réponse à une catégorie précise :

Network?
JavaScript?
Change detection?
Rendering?
Layout?
CPU?

Ce n’est qu’une fois que vous savez quel problème prédomine que vous devez choisir la technique appropriée. Après chaque modification, mesurez à nouveau pour confirmer que l’amélioration est réelle.

De une détection plus rapide des modifications à moins de travail

Les anciens conseils concernant les performances d’Angular tournaient principalement autour d’une seule question :

"How can I make change detection faster?"

Angular moderne encourage une question plus utile :

"Why am I running this work at all?"

Cette deuxième question s’applique à tous les niveaux. Pourquoi télécharger ce composant maintenant ? Pourquoi hydrater cette section maintenant ? Pourquoi laisser une requête HTTP se terminer alors que l’utilisateur a déjà modifié la requête ? Pourquoi invalider tout un objet d’état à cause d’un seul champ ? Pourquoi recalculer une valeur dont les entrées n’ont pas changé ? Pourquoi modifier le DOM avant que le rendu ne soit terminé ? Pourquoi effectuer des calculs lourds sur le thread UI ? Chaque opération inutile que vous identifiez représente une opportunité.

Points clés

Ces dix techniques ciblent différents niveaux et éliminent chacune un type spécifique de gaspillage :

  • @defer avec des importations directes réduit la quantité de JavaScript initiale.
  • L’hydratation incrémentale diminue le travail d’hydratation initial.
  • resource() avec un signal d’abandon supprime les tâches réseau obsolètes.
  • Les signaux fins limitent l’invalidation réactive.
  • computed() empêche les dérivation répétées.
  • Les services résolus de manière différée évitent le chargement précoce de dépendances lourdes.
  • Les phases afterRenderEffect() empêchent les problèmes de mise en page.
  • La fusion des événements et des exécutions élimine la détection redondante des modifications dans les applications basées sur des zones.
  • L’analyse des bundles révèle les dépendances accidentelles.
  • Les Web Workers transfèrent les tâches gourmandes en CPU hors du thread principal.
  • L’application Angular la plus rapide n’est pas celle qui exécute le JavaScript le plus rapidement ; c’est celle qui exécute le moins de JavaScript inutile. Toute la stratégie se résume en une image :

    MAKE ANGULAR FAST
                         │
              ┌──────────┴──────────┐
              │                     │
           DO LESS              DO LATER
              │                     │
        ┌─────┼─────┐         ┌─────┼─────┐
        │     │     │         │     │     │
      Less   Less  Less      Defer Lazy  Hydrate
      JS    CPU   DOM       code   data  later
        │     │     │         │     │     │
        └─────┴─────┴─────────┴─────┴─────┘
                         │
                         ▼
                  FASTER EXPERIENCE
    

    Mesurer, identifier le travail gaspillé, le supprimer, puis mesurer à nouveau.