<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Anatomie des performances du navigateur : Exemples concrets et analyse de code

Anatomie des performances du navigateur : exemples concrets et analyse de code

Dans le paysage du développement Web frontend moderne, la théorie du cycle de rendu et de la boucle d'événements doit nécessairement se traduire dans les pratiques de développement quotidiennes. Lorsque nous écrivons une application (que ce soit en JavaScript Vanilla, React ou Angular), chaque ligne qui touche le DOM ou gère l'asynchronie peut être la cause d'une interface fluide à 60 FPS ou d'une application complètement gelée.

Dans cette étude technique avancée, nous analyserons des exemples de code concrets qui montrent la différence entre les pratiques destructives et les stratégies optimisées, en examinant l'impact réel sur Reflow, Repaint, Event Loop, Virtual DOM et Zone.js.


1. Reflow vs Repaint : le désastre de la « mise en page synchrone forcée »

Le moyen le plus courant de détruire les performances du navigateur consiste à écrire du code qui alterne continuellement entre écriture (ce qui invalide la mise en page) et lecture (ce qui oblige le navigateur à le calculer immédiatement pour renvoyer une valeur mise à jour) des propriétés géométriques. Ce phénomène est appelé Layout Thrashing.

Code inefficace (provoque N reflux cycliques) :

// PESSIMA PRATICA: Modifica e legge la geometria nello stesso ciclo
const elements = document.querySelectorAll('.box');

for (let i = 0; i < elements.length; i++) {
  // Lettura: il browser deve calcolare il layout attuale
  const currentWidth = elements[i].offsetWidth; 
  
  // Scrittura: il browser invalida il layout appena calcolato
  elements[i].style.width = (currentWidth + 10) + 'px'; 
}

S'il y a 500 éléments dans le DOM avec la classe .box, ce script forcera le navigateur à effectuer 500 Reflows consécutifs sur la même frame. Le fil principal se figera instantanément.

La solution optimisée (Manual Batching) :

// OTTIMA PRATICA: Prima si legge tutto, poi si scrive tutto
const elements = document.querySelectorAll('.box');
const widths = [];

// Fase 1: Sola lettura (il browser calcola il layout una volta sola per tutti)
elements.forEach(el => {
  widths.push(el.offsetWidth);
});

// Fase 2: Sola scrittura (il browser esegue un unico Reflow globale alla fine)
elements.forEach((el, index) => {
  el.style.width = (widths[index] + 10) + 'px';
});

2. Boucle d'événement : bloquer le thread principal par rapport à requestAnimationFrame

Si vous exécutez des calculs lourds ou une animation time-lapse directement dans le thread principal sans synchronisation, la boucle d'événements ignorera la phase de rendu, provoquant des micro-goujons.

Code inefficace (Bloquer la boucle d'événement ou sauter des images) :

// PESSIMA PRATICA: setInterval non è sincronizzato con la frequenza del monitor
setInterval(() => {
  const box = document.getElementById('animate-me');
  let left = parseInt(box.style.left || 0);
  box.style.left = (left + 1) + 'px'; // Innesca Reflow continui senza controllo
}, 10); // Eseguito ogni 10ms, disallineato rispetto ai 16.67ms dei 60 FPS

La solution optimisée (Synchronisation avec rafraîchissement de l'écran) :

// OTTIMA PRATICA: requestAnimationFrame si adegua perfettamente al ciclo di rendering
let start;
const box = document.getElementById('animate-me');

function step(timestamp) {
  if (!start) start = timestamp;
  let progress = timestamp - start;
  
  // Usiamo transform (Composite) invece di 'left' per evitare Reflow e Repaint!
  box.style.transform = `translateX(${Math.min(progress / 10, 200)}px)`;
  
  if (progress < 2000) { // Continua l'animazione per 2 secondi
    window.requestAnimationFrame(step);
  }
}

// Il browser chiamerà questa funzione esattamente prima di ridisegnare lo schermo
window.requestAnimationFrame(step);

3. Le DOM virtuel à l'épreuve : Que se passe-t-il « sous le capot »

Pour comprendre pourquoi le DOM virtuel est efficace, voyons ce qui se passe lorsque nous devons mettre à jour une liste d'éléments en fonction des entrées de l'utilisateur.

Que ferait JavaScript natif (approche naïve) :

// Struttura HTML iniziale: <ul id="lista"><li>Item 1</li></ul>
const ul = document.getElementById('lista');

// Stato aggiornato: vogliamo aggiungere "Item 2" e modificare "Item 1"
// Soluzione drastica: svuotare e ricostruire (Distrugge il DOM e ricrea tutto)
ul.innerHTML = '<li>Item 1 Modificato</li><li>Item 2</li>'; 

Cette approche efface les nœuds existants, perd leurs états internes (tels que le focus d'entrée) et force une redistribution complète de l'ensemble du sous-arbre.

Ce que fait l'algorithme de réconciliation (Virtual DOM) en arrière-plan :

// 1. Rappresentazione in memoria del vecchio stato (VNode)
const oldVNode = {
  type: 'ul',
  children: [{ type: 'li', text: 'Item 1' }]
};

// 2. Nuovo stato generato dopo l'azione dell'utente
const newVNode = {
  type: 'ul',
  children: [
    { type: 'li', text: 'Item 1 Modificato' },
    { type: 'li', text: 'Item 2' }
  ]
};

// 3. Il Framework esegue il Diffing ed esegue SOLO le operazioni mirate sul DOM reale:
// - UpdateTextNode su children[0] ('Item 1' -> 'Item 1 Modificato')
// - CreateElement e Append su children[1] ('Item 2')

4. Zone.js en Angular : Le coût de l'asynchronie incontrôlée

Zone.js intercepte tout événement. Si vous disposez d'une fonction qui surveille les mouvements de la souris, Zone.js déclenchera une détection globale des changements sur chaque pixel déplacé, détruisant ainsi les performances de l'application.

Code inefficace (effectue des centaines de vérifications inutiles par seconde) :

import { Component } from '@angular/core';

@Component({
  selector: 'app-mouse-tracker',
  template: `<div (mousemove)="onMouseMove($event)">Tracciami</div>`
})
export class MouseTrackerComponent {
  onMouseMove(event: MouseEvent) {
    // Ogni volta che muovi il mouse, Zone.js intercetta l'evento
    // e Angular controlla TUTTI i componenti della pagina!
    console.log('Mouse in posizione:', event.clientX);
  }
}

La solution optimisée (Sortie de la « Zone » angulaire) :

import { Component, OnInit, NgZone, ElementRef, ViewChild } from '@angular/core';

@Component({
  selector: 'app-mouse-tracker',
  template: `<div #tracker>Tracciami in sicurezza</div>`
})
export class MouseTrackerComponent implements OnInit {
  @ViewChild('tracker', { static: true }) tracker!: ElementRef;

  constructor(private ngZone: NgZone) {}

  ngOnInit() {
    // Eseguiamo l'ascolto dell'evento FUORI da Zone.js
    this.ngZone.runOutsideAngular(() => {
      this.tracker.nativeElement.addEventListener('mousemove', (event: MouseEvent) => {
        // Il browser esegue il codice, ma Angular NON avvia la Change Detection.
        // Perfetto per performance estreme.
        this.tracker.nativeElement.innerText = `X: ${event.clientX}`;
      });
    });
  }
}

💬 Notes des lecteurs

0 notes

Écrire une note

Partagez votre avis, une suggestion ou un compliment

Dernières notes

Aucune note pour le moment. Soyez le premier à commenter !