Cours JavaScript et TypeScript · Module 8: Événements et DOM

Performances et optimisation des événements

5 min de lecture

Comment garantir de hautes performances quand on utilise intensivement les événements.

« Un parc équipé d'un millier de capteurs ne fonctionne que si aucun d'eux ne bloque la ligne de communication principale », dit Ray Arnold. Dans le navigateur, cette ligne, c'est le thread principal. Ce même thread exécute ton JavaScript et gère le défilement, donc un écouteur trop lourd peut faire défiler une page par à-coups sur un téléphone. Dans cette leçon, nous rassemblons les outils qui soulagent ce thread.

Optimisations des performances

Le premier outil, c'est l'écouteur passif. L'option { passive: true } est une promesse faite au navigateur : cet écouteur n'appellera jamais preventDefault().

1// Écouteurs passifs pour de meilleures performances du défilement
2window.addEventListener('scroll', handleScroll, { passive: true });
3window.addEventListener('touchstart', handleTouch, { passive: true });

Pour les événements qui peuvent arrêter le défilement, c'est-à-dire touchstart, touchmove et wheel, le navigateur devrait normalement attendre que l'écouteur termine son travail, car celui-ci pourrait bloquer le défilement. Après une telle promesse, le défilement démarre tout de suite, sans attendre l'écouteur. Si un écouteur passif appelle malgré tout preventDefault(), le navigateur ignorera cet appel. Pour scroll seul, le drapeau n'accélère rien, car cet événement ne peut de toute façon pas être annulé, mais il ne fait pas de mal et décrit clairement l'intention.

L'objet d'options connaît d'autres drapeaux. Chacun vaut false par défaut, et on peut les combiner dans un seul objet :

1// Écouteur exécuté une seule fois
2button.addEventListener('click', handleClick, { once: true });
3
4// Capture phase
5button.addEventListener('click', handleClick, { capture: true });
6
7// Combinaison d'options
8element.addEventListener('scroll', handleScroll, {
9  passive: true,
10  capture: false,
11  once: false
12});

L'option once supprime l'écouteur après le premier appel, donc pas besoin de penser au nettoyage. L'option capture fait passer l'écouteur dans la phase de capture, que tu connais depuis la leçon sur la propagation. En soi, elle n'accélère pas le code, mais elle fait partie du même objet d'options. Les navigateurs traitent touchstart, touchmove et wheel sur window, document, document.documentElement et body comme passifs, même sans cette option. C'est pourquoi je préfère toujours écrire passive: true explicitement.

Intersection Observer au lieu de scroll

La méthode classique pour détecter qu'un élément est apparu à l'écran, c'est un écouteur scroll qui mesure la position de l'élément à chaque mouvement. IntersectionObserver inverse cette organisation : c'est le navigateur qui te prévient lui-même quand la visibilité de l'élément change.

1// Intersection Observer au lieu des événements scroll
2const observer = new IntersectionObserver((entries) => {
3  entries.forEach(entry => {
4    if (entry.isIntersecting) {
5      // L'élément est entré dans le viewport
6      loadLazyContent(entry.target);
7    }
8  });
9}, {
10  rootMargin: '100px' // Déclenchement 100px avant l'entrée dans le viewport
11});
12
13// Observer les éléments
14document.querySelectorAll('.lazy-load').forEach(el => {
15  observer.observe(el);
16});

Le callback reçoit un tableau entries, avec une entrée pour chaque élément dont la visibilité a changé. La propriété isIntersecting indique si l'élément se trouve maintenant dans la zone d'affichage, c'est-à-dire dans le viewport. L'option rootMargin: '100px' élargit cette zone de 100 pixels, donc le contenu commence à se charger avant que le visiteur n'y arrive. Un seul observateur peut surveiller autant d'éléments que tu veux, et ton callback ne s'exécute pas entre deux changements. C'est sur ce principe que repose le chargement différé des images : loadLazyContent() peut recopier l'adresse de l'attribut data-src dans src, puis appeler observer.unobserve(), car une image chargée n'a plus besoin d'être observée.

ResizeObserver au lieu de resize

L'événement resize sur window ne parle que du changement de taille de la fenêtre. Or un panneau peut se rétrécir parce qu'une barre latérale s'est déployée à côté de lui, alors que la fenêtre est restée la même.

1// ResizeObserver au lieu des événements resize
2const resizeObserver = new ResizeObserver(entries => {
3  entries.forEach(entry => {
4    const { width, height } = entry.contentRect;
5    updateElementLayout(entry.target, width, height);
6  });
7});
8
9resizeObserver.observe(document.getElementById('responsive-element'));

ResizeObserver signale le changement de taille d'un élément précis, quelle qu'en soit la cause. Dans contentRect, on lit la largeur et la hauteur de la zone de contenu, et on n'ajuste la mise en page que lorsqu'elle a réellement changé. Nous verrons les deux observateurs plus en détail dans la leçon sur les API DOM avancées.

L'ordre des décisions

Mon conseil, quand une page ralentit à cause des événements : demande-toi d'abord si l'écouteur scroll est vraiment nécessaire, car un observateur peut souvent le remplacer. S'il est nécessaire, limite-le avec le throttling. Ajoute passive: true aux écouteurs du tactile et de la molette. Au lieu de centaines d'écouteurs sur les cartes, utilise la délégation. Chaque étape réduit le travail du thread principal.

Dans le bac à sable ci-dessous, tu verras les écouteurs passifs et l'Intersection Observer en action. Dans la prochaine leçon, tu verras comment les événements organisent l'architecture de toute une application grâce au motif Event Bus.

Retiens bien : le capteur le plus rapide est celui qui ne bloque pas la ligne de communication et ne se manifeste que lorsqu'il se passe vraiment quelque chose.

Code de cette leçon : index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <title>Performances et optimisation</title>
6    <style>
7        body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; }
8        .demo { background: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; margin: 20px 0; }
9        .scroll-box { height: 200px; overflow-y: auto; background: rgba(255,255,255,0.05); padding: 15px; border-radius: 6px; }
10        .stat { display: inline-block; margin: 10px; padding: 10px 20px; background: rgba(52, 152, 219, 0.2); border-radius: 6px; }
11        .dino-item { padding: 20px; margin: 10px 0; background: rgba(255,255,255,0.05); border-radius: 6px; }
12    </style>
13</head>
14<body>
15    <h1>Performances et optimisation</h1>
16
17    <div class="demo">
18        <h3>Écouteurs d'événements passifs</h3>
19        <div class="scroll-box" id="passive-scroll">
20            <div style="height: 1000px;">
21                <p>Fais-moi défiler ! (Écouteur passif = meilleures performances)</p>
22                <p style="margin-top: 200px;">Continue à défiler...</p>
23                <p style="margin-top: 200px;">Presque fini...</p>
24                <p style="margin-top: 200px;">Bien joué !</p>
25            </div>
26        </div>
27        <div class="stat" id="scroll-count">Scrolls: 0</div>
28    </div>
29
30    <div class="demo">
31        <h3>Intersection Observer</h3>
32        <div class="scroll-box" id="observer-container">
33            <div class="dino-item" data-dino="T-Rex">T-Rex (not visible)</div>
34            <div class="dino-item" data-dino="Velociraptor">Velociraptor (not visible)</div>
35            <div class="dino-item" data-dino="Triceratops">Triceratops (not visible)</div>
36            <div class="dino-item" data-dino="Brachiosaurus">Brachiosaurus (not visible)</div>
37        </div>
38        <div class="stat" id="observer-count">Loaded: 0</div>
39    </div>
40
41    <script>
42        console.log('Performance optimization demo');
43
44        // PASSIVE EVENT LISTENERS
45        let scrollCount = 0;
46        const passiveScroll = document.getElementById('passive-scroll');
47
48        passiveScroll.addEventListener('scroll', () => {
49            scrollCount++;
50            document.getElementById('scroll-count').textContent = `Scrolls: ${scrollCount}`;
51        }, { passive: true }); // Passive for better performance
52
53        // INTERSECTION OBSERVER (au lieu des scroll events)
54        let loadedCount = 0;
55        const observer = new IntersectionObserver((entries) => {
56            entries.forEach(entry => {
57                if (entry.isIntersecting) {
58                    const dinoName = entry.target.dataset.dino;
59                    entry.target.textContent = `${entry.target.textContent.split('(')[0]}(LOADED!)`;
60                    entry.target.style.background = 'rgba(46, 204, 113, 0.2)';
61                    loadedCount++;
62                    document.getElementById('observer-count').textContent = `Loaded: ${loadedCount}`;
63                    console.log(`${dinoName} entered viewport`);
64                    observer.unobserve(entry.target); // Stop observing after loading
65                }
66            });
67        }, {
68            threshold: 0.5 // Trigger quand 50% de l'élément est visible
69        });
70
71        // Observe all dinosaur elements
72        document.querySelectorAll('.dino-item').forEach(item => {
73            observer.observe(item);
74        });
75
76        // ONCE OPTION - event listener une seule fois
77        document.body.addEventListener('click', () => {
78            console.log('This fires only ONCE');
79            alert('This alert will appear only once!');
80        }, { once: true });
81
82        // RESIZE OBSERVER (au lieu des resize events)
83        const resizeObserver = new ResizeObserver(entries => {
84            entries.forEach(entry => {
85                const { width, height } = entry.contentRect;
86                console.log(`Container resized: ${Math.round(width)}x${Math.round(height)}`);
87            });
88        });
89
90        resizeObserver.observe(document.getElementById('observer-container'));
91
92        console.log('All performance optimizations active');
93        console.log('Using: Passive listeners, Intersection Observer, Resize Observer');
94    </script>
95</body>
96</html>

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que signifie l'option { passive: true } dans addEventListener ?

  2. 2. À quoi sert IntersectionObserver ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Le fichier index.js contient deux fonctions. setupLazyLoading(images) doit observer les images et copier l'adresse de data-src vers src seulement quand une image entre dans la zone visible. setupScrollProgress(target, onProgress) ajoute un écouteur scroll qui transmet scrollTop à onProgress. Complète les blancs : ___BLANK1___ est le constructeur de l'observateur de visibilité, ___BLANK2___ est la méthode de l'observateur qui arrête d'observer une image chargée, et ___BLANK3___ est l'option de addEventListener par laquelle l'écouteur promet de ne pas appeler preventDefault().

  • Clique dans l'ordre

    Place les fragments de code pour construire l'objet d'options { passive: true } :

  • Clique dans l'ordre

    Place la syntaxe d'un écouteur d'événement passif :

Articles utiles