All posts

Anatomia delle Performance del Browser: Esempi Concreti e Analisi del Codice

Anatomia delle Performance del Browser: Esempi Concreti e Analisi del Codice

Nel panorama del moderno sviluppo web frontend, la teoria del ciclo di rendering e dell'Event Loop deve necessariamente tradursi in pratiche di sviluppo quotidiane. Quando scriviamo un'applicazione (sia essa in vanilla JavaScript, React o Angular), ogni riga che tocca il DOM o che gestisce l'asincronia può essere la causa di un'interfaccia fluida a 60 FPS o di un'applicazione completamente bloccata.

In questo approfondimento tecnico avanzato, analizzeremo esempi concreti di codice che mostrano la differenza tra pratiche distruttive e strategie ottimizzate, sviscerando l'impatto reale su Reflow, Repaint, Event Loop, Virtual DOM e Zone.js.


1. Reflow vs Repaint: Il disastro del "Forced Synchronous Layout"

Il modo più comune per distruggere le performance del browser è scrivere codice che alterna continuamente la scrittura (che invalida il layout) e la lettura (che costringe il browser a calcolarlo immediatamente per restituire un valore aggiornato) delle proprietà geometriche. Questo fenomeno prende il nome di Layout Thrashing.

Il codice inefficiente (Provoca N Reflow ciclici):

// 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'; 
}

Se nel DOM ci sono 500 elementi con la classe .box, questo script costringerà il browser a eseguire 500 Reflow consecutivi sullo stesso frame. Il thread principale si congelerà istantaneamente.

La soluzione ottimizzata (Batching manuale):

// 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. Event Loop: Bloccare il thread principale vs requestAnimationFrame

Se esegui un calcolo pesante o un'animazione temporizzata direttamente nel thread principale senza sincronizzazione, l'Event Loop salterà la fase di rendering causando micro-scatti.

Il codice inefficiente (Blocca l'Event Loop o salta i frame):

// 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 soluzione ottimizzata (Sincronizzazione con il refresh dello schermo):

// 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. Virtual DOM alla prova: Cosa succede "sotto il cofano"

Per capire perché il Virtual DOM è efficiente, vediamo cosa accade quando dobbiamo aggiornare una lista di elementi in base a un input utente.

Cosa farebbe JavaScript nativo (Approccio 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>'; 

Questo approccio cancella i nodi esistenti, perde i loro stati interni (come il focus dell'input) e forza un Reflow completo dell'intero sottoalbero.

Cosa fa l'algoritmo di Riconciliazione (Virtual DOM) in background:

// 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 in Angular: Il costo dell'asincronia incontrollata

Zone.js intercetta qualsiasi evento. Se hai una funzione che monitora un movimento del mouse, Zone.js scatenerà la Change Detection globale ad ogni singolo pixel spostato, distruggendo le performance dell'applicazione.

Il codice inefficiente (Esegue centinaia di controlli inutili al secondo):

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 soluzione ottimizzata (Uscire dalla "Zona" di Angular):

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}`;
      });
    });
  }
}