IFT3225

Une application peut être fonctionnelle et bien structurée, mais tout de même rester pénible à utiliser dûs à des problèmes de performance. Certains problèmes peuvent être corrigés tardivement, mais les corrections deviennent plus coûteuses lorsque l’application a été construite autour de dépendances lourdes ou d’une succession d’échanges réseau.

1. Mesurer et diagnostiquer avec DevTools

Une page peut sembler lente à cause d’une police demandée tardivement, d’une image surdimensionnée, d’un appel d’API en série ou d’une tâche JavaScript qui bloque le fil principal. Les instruments de DevTools montrent des dimensions différentes du même chargement.

Le panneau Réseau : suivre les requêtes

Le panneau Network enregistre les requêtes effectuées par la page. Chaque ligne correspond à une ressource ou à un échange : document, feuille de style, script, image, police ou appel d’API. La cascade placée à droite montre quand la requête commence, combien de temps elle attend et à quel moment elle se termine.

Mesures à observer

Lighthouse : obtenir un bilan automatisé

Lighthouse exécute une série d’audits dans des conditions contrôlées. Il fournit des métriques, détecte des problèmes fréquents et associe certaines recommandations aux ressources concernées. Il constitue un bon point de départ, mais il ne remplace pas l’analyse détaillée dans les panneaux Réseau et Performance.

Mesures à observer

Le panneau Performance : analyser le travail du navigateur

Le panneau Performance enregistre l’activité du navigateur pendant le chargement ou pendant une interaction. Il montre les requêtes, l’exécution du JavaScript, les recalculs de style, les mises en page, la peinture et la composition des images à l’écran.

Éléments à observer

2. Choisir une stratégie d’optimisation

Les technologies changent rapidement, mais la plupart des interventions reposent sur quelques stratégies stables. Les reconnaitre permet de choisir une solution en fonction du problème observé plutôt que de collectionner des réglages.

  1. Éliminer ce qui n’apporte pas suffisamment de valeur.
  2. Réduire le poids ou la quantité de travail de ce qui demeure.
  3. Différer ce qui n’est pas encore nécessaire.
  4. Devancer ce qui sera bientôt nécessaire et découvert trop tard.
  5. Réutiliser une ressource, une donnée ou un résultat déjà obtenu.
  6. Rapprocher les ressources et les services de leurs utilisateurs.

L’ordre est une heuristique, non une règle absolue. Supprimer une ressource produit généralement un gain plus complet que la compresser. Une ressource indispensable ne peut toutefois pas être éliminée, et une ressource secondaire peut être plus efficacement différée que minutieusement réduite.

2.1 Éliminer

La ressource la plus rapide est celle qui n’est jamais demandée, et le calcul le plus rapide est celui qui n’est jamais effectué.

Éliminer par la conception

Éliminer manuellement dans le code

Le tree shaking

Le tree shaking, ou élagage, est une forme d’élimination automatique du code mort pendant la construction. L’outil analyse les imports et exports des modules, marque les éléments nécessaires au programme et exclut les portions qui ne contribuent pas au résultat final.

// math.js
export function addition(a, b) {
  return a + b;
}

export function multiplication(a, b) {
  return a * b;
}

// main.js
import { addition } from "./math.js";

console.log(addition(2, 3));

Dans un cas simple, la fonction multiplication peut être absente du bundle de production puisqu’elle n’est jamais importée.

Des outils de construction comme Vite, Rollup, webpack et esbuild appliquent ce type d’analyse pendant la production d’un bundle. Le résultat est généralement meilleur lorsque le code utilise des modules ES statiques et que les modules déclarent correctement leurs effets de bord.

2.2 Réduire

Ce qui demeure nécessaire doit transporter moins d’octets et demander aussi peu de travail que possible. Les techniques diffèrent selon le type de ressource.

Réduire le document HTML

Réduire le CSS

Réduire le JavaScript

Réduire les images

<img
  src="/images/projet-800.webp"
  srcset="
    /images/projet-480.webp 480w,
    /images/projet-800.webp 800w,
    /images/projet-1200.webp 1200w
  "
  sizes="(max-width: 640px) 100vw, 800px"
  width="1200"
  height="800"
  alt="Présentation du projet"
/>

Les attributs width et height ne réduisent pas le fichier, mais permettent au navigateur de réserver l’espace avant l’arrivée de l’image et limitent les déplacements visuels.

Réduire les polices

Réduire les réponses d’API

2.3 Différer

Différer consiste à reporter une ressource ou un traitement qui n’est pas nécessaire à l’affichage ou à l’interaction immédiate. Le chargement a lieu plus tard, lorsqu’un besoin devient probable ou réel.

Images et cadres chargés paresseusement

<img
  src="/images/equipe.webp"
  alt="L'équipe du projet"
  width="1200"
  height="800"
  loading="lazy"
/>

<iframe
  src="https://exemple.ca/carte"
  title="Carte du secteur"
  loading="lazy"
></iframe>

L’attribut loading="lazy" permet au navigateur de reporter le chargement jusqu’à ce que la ressource approche de la zone visible. Il ne devrait pas être appliqué à l’image principale visible dès l’ouverture de la page, puisque cette image doit au contraire être découverte et chargée rapidement.

Scripts classiques : chargement et exécution

Inclusion Téléchargement Exécution Ordre entre plusieurs scripts
<script src="app.js"> l’analyse du document peut être interrompue dès que le script est reçu ordre du document
<script defer src="app.js"> en parallèle de l’analyse après l’analyse du document ordre conservé
<script async src="mesure.js"> en parallèle de l’analyse dès que le script est prêt ordre non garanti
<script type="module" src="app.js"> en parallèle avec ses dépendances différée par défaut dépendances du graphe de modules

defer convient généralement à des scripts qui dépendent du document complet ou les uns des autres. async convient mieux à un script indépendant, comme certains outils de mesure. Un module JavaScript est différé par défaut, mais ses imports peuvent former leur propre graphe de dépendances.

Découpage du code et imports dynamiques

Le code splitting place des parties de l’application dans des fichiers séparés, chargés seulement lorsque leur fonctionnalité est demandée.

import { lazy, Suspense } from "react";

const VueAdmin = lazy(() => import("./pages/VueAdmin.jsx"));

export function Administration() {
  return (
    <Suspense fallback={<p>Chargement…</p>}>
      <VueAdmin />
    </Suspense>
  );
}

2.4 Devancer

Devancer consiste à commencer plus tôt une opération que le navigateur ou l’application découvrirait naturellement trop tard. Cette stratégie s’applique seulement lorsqu’on a de bonnes raisons de croire que la ressource sera nécessaire.

Indications de chargement dans le document

Indication Usage principal
preload ressource importante pour la page courante, découverte trop tard
modulepreload module JavaScript et, selon le navigateur, préparation de ses dépendances
preconnect connexion anticipée vers une origine qui sera bientôt utilisée
prefetch ressource probablement utile lors d’une navigation future
<link
  rel="preload"
  href="/fonts/inter-latin.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

<link rel="modulepreload" href="/assets/editeur.js" />
<link rel="preconnect" href="https://api.exemple.ca" />
<link rel="prefetch" href="/assets/vue-projet.js" />

Une image importante peut également recevoir un indice de priorité :

<img
  src="/images/accueil.webp"
  width="1600"
  height="900"
  fetchpriority="high"
  alt="Campus vu depuis l'entrée principale"
/>

fetchpriority="high" ne garantit pas un ordre absolu. Il fournit une information supplémentaire au navigateur, qui conserve la décision finale en fonction des autres ressources.

Devancer dans le code JavaScript

Une cascade peut être créée par le code lui-même :

const profil = await chargerProfil();
const projets = await chargerProjets();

Si les deux opérations sont indépendantes, elles peuvent commencer ensemble :

const [profil, projets] = await Promise.all([
  chargerProfil(),
  chargerProjets(),
]);

Autres stratégies courantes :

2.5 Réutiliser

Réutiliser consiste à éviter un transfert, une lecture ou un calcul dont le résultat est déjà disponible et encore valide.

Au niveau du réseau, cette stratégie mène aux caches HTTP, aux CDN, aux caches applicatifs et aux caches serveur. Ces mécanismes sont étudiés séparément dans la page consacrée au cache et aux applications web progressives.

La réutilisation existe aussi dans le code :

Mémoïsation dans React

useMemo peut conserver le résultat d’un calcul entre les rendus :

import { useMemo } from "react";

function ListeProjets({ projets, filtre }) {
  const projetsVisibles = useMemo(
    () => projets.filter((projet) => correspond(projet, filtre)),
    [projets, filtre],
  );

  return projetsVisibles.map((projet) => (
    <CarteProjet key={projet.id} projet={projet} />
  ));
}

2.6 Rapprocher

À défaut d’éliminer un échange, on peut réduire la distance et le nombre d’intermédiaires entre l’utilisateur et la ressource.

Un réseau de distribution de contenu, ou CDN, conserve des copies des fichiers statiques dans plusieurs points de présence. L’utilisateur reçoit alors le HTML, le CSS, le JavaScript ou les images depuis une infrastructure plus proche.

Pour les API et les bases de données, rapprocher peut signifier :

La proximité ne corrige toutefois pas une longue cascade ni un traitement inefficace. Un serveur proche qui attend successivement trois autres services peut rester plus lent qu’un serveur légèrement plus éloigné qui répond directement.

3. Une méthode d’optimisation

Une démarche reproductible évite de modifier plusieurs facteurs sans savoir lequel a produit le gain.

  1. Définir un scénario. Par exemple : ouvrir la page d’accueil sur un téléphone simulé, afficher un projet ou filtrer une liste.
  2. Mesurer une référence. Conserver la cascade, les métriques et, au besoin, une trace Performance.
  3. Localiser le coût dominant. Réseau, poids, découverte tardive, JavaScript, rendu ou infrastructure.
  4. Choisir une stratégie. Éliminer, réduire, différer, devancer, réutiliser, limiter ou rapprocher.
  5. Modifier un facteur important. Éviter une série de microchangements impossibles à attribuer.
  6. Mesurer de nouveau dans les mêmes conditions. Vérifier le gain et les effets secondaires.
  7. Tester un autre scénario. Une optimisation de la première visite ne doit pas détériorer une interaction ou une navigation suivante.