Anatomy of Browser Performance: Concrete Examples and Code Analysis
In modern frontend web development, the theory of the rendering cycle and the Event Loop must translate into everyday development practices. When we write an application (whether in vanilla JavaScript, React or Angular), every line that touches the DOM or handles asynchrony can be the cause of a smooth 60 FPS interface or of a completely frozen application.
In this advanced technical deep dive, we will analyze concrete code examples that show the difference between destructive practices and optimized strategies, breaking down the real impact on Reflow, Repaint, the Event Loop, the Virtual DOM and Zone.js.
1. Reflow vs Repaint: the "Forced Synchronous Layout" disaster
The most common way to destroy browser performance is to write code that keeps alternating between writing geometric properties (which invalidates the layout) and reading them (which forces the browser to compute the layout immediately to return an up-to-date value). This phenomenon is called Layout Thrashing.
The inefficient code (causes N cyclic reflows):
// 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';
}
If there are 500 elements with the class .box in the DOM, this script will force the browser to run 500 consecutive reflows in the same frame. The main thread will freeze instantly.
The optimized solution (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. Event Loop: blocking the main thread vs requestAnimationFrame
If you run a heavy computation or a timed animation directly on the main thread without synchronization, the Event Loop will skip the rendering phase, causing micro-stutters.
The inefficient code (blocks the Event Loop or skips frames):
// 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
The optimized solution (synchronized with the screen refresh):
// 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. The Virtual DOM put to the test: what happens "under the hood"
To understand why the Virtual DOM is efficient, let's look at what happens when we need to update a list of elements based on user input.
What native JavaScript would do (naive approach):
// 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>';
This approach deletes the existing nodes, loses their internal state (such as input focus) and forces a full reflow of the entire subtree.
What the reconciliation algorithm (Virtual DOM) does in the 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: the cost of uncontrolled asynchrony
Zone.js intercepts every event. If you have a function that tracks mouse movement, Zone.js will trigger global change detection for every single pixel moved, destroying the application's performance.
The inefficient code (runs hundreds of useless checks per second):
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);
}
}
The optimized solution (stepping out of Angular's "zone"):
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}`;
});
});
}
}