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

Anatomía del rendimiento del navegador: Ejemplos concretos y análisis de código

Anatomía del rendimiento del navegador: ejemplos concretos y análisis de código

En el panorama del desarrollo web frontend moderno, la teoría del ciclo de renderizado y el Event Loop deben necesariamente traducirse en prácticas de desarrollo diarias. Cuando escribimos una aplicación (ya sea en JavaScript básico, React o Angular), cada línea que toca el DOM o maneja la asincronía puede ser la causa de una interfaz fluida a 60 FPS o una aplicación completamente congelada.

En este estudio técnico avanzado, analizaremos ejemplos de código concretos que muestran la diferencia entre prácticas destructivas y estrategias optimizadas, examinando el impacto real en Reflow, Repaint, Event Loop, Virtual DOM y Zone.js.


1. Reflujo vs Repintado: el desastre del "diseño sincrónico forzado"

La forma más común de destruir el rendimiento del navegador es escribir código que alterne continuamente entre writing (que invalida el diseño) y reading (que obliga al navegador a calcularlo inmediatamente para devolver un valor actualizado) de propiedades geométricas. Este fenómeno se llama Layout Thrashing.

Código ineficiente (provoca N reflujos cíclicos):

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

Si hay 500 elementos en el DOM con la clase .box, este script obligará al navegador a realizar 500 reflujos consecutivos en el mismo marco. El hilo principal se congelará instantáneamente.

La solución optimizada (procesamiento por lotes manual):

// 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. Bucle de eventos: bloquear el hilo principal frente a requestAnimationFrame

Si ejecuta cálculos pesados o animaciones de lapso de tiempo directamente en el hilo principal sin sincronización, el bucle de eventos omitirá la fase de renderizado causando micro-studs.

Código ineficiente (Bloquear bucle de eventos u omitir fotogramas):

// 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 solución optimizada (Sincronización con actualización de pantalla):

// 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. DOM virtual puesto a prueba: qué sucede "bajo el capó"

Para entender por qué el DOM virtual es eficiente, veamos qué sucede cuando necesitamos actualizar una lista de elementos según la entrada del usuario.

Qué haría JavaScript nativo (enfoque ingenuo):

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

Este enfoque borra los nodos existentes, pierde sus estados internos (como el foco de entrada) y fuerza un reflujo completo de todo el subárbol.

Qué hace el algoritmo de reconciliación (DOM virtual) en segundo plano:

// 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: El costo de la asincronía incontrolada

Zone.js intercepta cualquier evento. Si tiene una función que monitorea el movimiento del mouse, Zone.js activará la detección de cambios global en cada píxel movido, destruyendo el rendimiento de la aplicación.

Código ineficiente (Realiza cientos de comprobaciones inútiles por segundo):

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 solución optimizada (Saliendo de la "Zona" 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}`;
      });
    });
  }
}

💬 Notas de los lectores

0 notas

Escribe una nota

Comparte tu opinión, una sugerencia o un cumplido

Últimas notas

Aún no hay notas. ¡Sé el primero en comentar!