Ottimizzazione delle Performance

17 febbraio 2026
16 min di lettura

Introduzione

Scrivere codice JavaScript funzionante è solo il primo passo. È altrettanto importante scrivere codice che offra buone performance agli utenti finali e identificare eventuali problemi che possono compromettere l’esperienza utente.

In questo capitolo si approfondiscono:

  • Cosa sono le performance: startup time e runtime performance
  • Strumenti di misurazione: browser dev tools, performance.now, jsperf.com
  • Ottimizzazioni startup time: riduzione dimensione script, lazy loading, code splitting
  • Ottimizzazioni runtime: manipolazione DOM efficiente, prevenzione memory leaks
  • Micro-ottimizzazioni: quando usarle e quando evitarle
  • Ottimizzazioni server-side: compressione, caching, HTTP/2

Cosa sono le Performance

Quando si parla di performance nel contesto di un sito web o di un’applicazione web, il termine può riferirsi a molti aspetti diversi. Possiamo identificare due categorie principali:

Startup Time

Lo startup time riguarda la velocità con cui un’applicazione diventa utilizzabile:

  • Tempo di visualizzazione: quanto tempo passa prima che l’utente veda qualcosa sullo schermo
  • Tempo di interazione: quanto tempo passa prima che l’utente possa interagire con la pagina

Un sito lento nel caricamento o che sembra caricato ma non risponde ai click offre una pessima esperienza utente.

Runtime Performance

Una volta che la pagina è caricata, l’applicazione deve offrire performance fluide:

  • Fluidità generale: l’applicazione non deve congelare o laggare occasionalmente
  • Responsività input: l’input dell’utente deve essere catturato senza ritardi
  • Animazioni fluide: le animazioni devono essere smooth, senza lag visivo
  • Assenza di memory leaks: evitare che parti del codice occupino sempre più memoria nel tempo

Performance non è solo JavaScript

Le performance di un’applicazione web sono influenzate da molti fattori:

  • CSS: selettori complessi richiedono più tempo al browser per applicare gli stili
  • HTML: codice HTML non necessario aumenta il tempo di download iniziale
  • JavaScript: influisce sia sullo startup time che sul runtime performance
  • Server: velocità e configurazione del server influenzano i tempi di risposta
  • Geolocalizzazione: server lontani dagli utenti introducono latenza aggiuntiva

In questo capitolo ci concentriamo principalmente sulle performance JavaScript, ma è importante ricordare che esistono altri fattori che giocano un ruolo importante.


Performance JavaScript: Startup Time vs Runtime

Quando parliamo di performance JavaScript, possiamo distinguere due aree principali:

Startup Time

Riguarda la velocità con cui lo script viene caricato ed eseguito:

  • Velocità di download: quanto tempo serve per scaricare il file JavaScript
  • Velocità di esecuzione: quanto tempo serve per eseguire il codice iniziale

Questo aspetto è principalmente rilevante per il browser, dove il codice deve essere scaricato prima di essere eseguito. Su Node.js questo problema è meno rilevante perché il codice è già sul server.

Runtime Performance

Riguarda l’efficienza del codice durante l’esecuzione:

  • Efficienza del codice: quanto velocemente viene eseguito il lavoro richiesto
  • Interazioni con il DOM: operazioni DOM inefficienti possono rallentare l’applicazione
  • Memory leaks: accumulo di memoria nel tempo può portare a rallentamenti o crash

Questo aspetto è rilevante sia per il browser che per Node.js, anche se alcuni problemi (come le interazioni DOM) sono specifici del browser.


Fattori che Influenzano le Performance

Startup Time

Dimensione degli Script

La dimensione del file JavaScript è un fattore critico:

  • File più grandi richiedono più tempo per il download
  • Anche file relativamente piccoli (100kb) possono richiedere alcuni secondi su connessioni lente
  • Non tutti gli utenti hanno connessioni veloci o dispositivi potenti

La dimensione dello script influisce sia sul tempo di visualizzazione che sul tempo di interazione, specialmente se lo script è necessario per aggiungere event listener agli elementi interattivi.

Numero di HTTP Round Trips

Ogni richiesta HTTP ha un costo base di setup, indipendentemente dalla dimensione del file scaricato:

  • Più file JavaScript significa più richieste HTTP
  • Ogni richiesta aggiunge latenza al tempo di caricamento totale
  • Il bundling (ad esempio con webpack) riduce il numero di richieste combinando i file

Runtime Performance

Accesso al DOM

Le operazioni DOM sono costose dal punto di vista delle performance:

  • Ogni operazione DOM richiede comunicazione tra JavaScript e il browser
  • Il browser deve attraversare il DOM, trovare gli elementi, crearli o modificarli
  • Re-renderizzare troppi elementi quando ne basta uno può rallentare significativamente l’applicazione

Memory Leaks

I memory leak possono accumularsi nel tempo:

  • Non tutti i leak sono immediatamente evidenti
  • Piccoli leak possono diventare problematici in applicazioni che girano a lungo
  • Leak più grandi possono portare a rallentamenti o crash del browser

Alternative di Codice

Diverse soluzioni allo stesso problema possono avere performance diverse:

  • Alcuni metodi di iterazione su array sono più veloci di altri
  • La scelta della struttura dati può influenzare le performance
  • Queste sono spesso micro-ottimizzazioni che vanno usate con cautela

Misurare le Performance

Prima di ottimizzare, è fondamentale misurare e identificare i colli di bottiglia. Non bisogna indovinare, ma misurare.

Approccio Generale

  1. Audit della pagina: identificare quanti round trip ci sono, quanto è grande lo script da scaricare
  2. Budget di performance: stabilire obiettivi (es. script sotto 100kb)
  3. Misurazione runtime: usare browser dev tools per identificare operazioni lente
  4. Best practices e benchmark: consultare risorse online per confrontare approcci alternativi

Misurare Codice Production-Ready

Importante: misurare sempre il codice production-ready, non quello in modalità sviluppo.

In sviluppo, strumenti come webpack dev server iniettano codice extra per migliorare il debugging, ma questo codice non è presente in produzione e può dare risultati fuorvianti.

Strumenti di Misurazione

Performance.now()

Permette di ottenere uno snapshot temporale nel browser:

const startTime = performance.now();
// Operazione da misurare
const endTime = performance.now();
console.log(`Operazione completata in ${endTime - startTime} millisecondi`);

Browser Dev Tools

I moderni browser offrono strumenti avanzati per l’analisi delle performance:

  • Performance tab: registrazione e analisi dettagliata delle operazioni
  • Memory tab: snapshot della memoria per identificare leak
  • Network tab: analisi del download e dei round trip
  • Coverage tab: identificazione del codice non utilizzato

jsperf.com

Sito web per confrontare benchmark di alternative di codice JavaScript. Permette di:

  • Creare test case personalizzati
  • Confrontare diversi approcci (es. for vs forEach)
  • Ottenere risultati medi su migliaia di esecuzioni

webpagetest.org

Sito web per testare siti hostati (non locali) e ottenere report dettagliati sulle performance, inclusi:

  • Tempo di caricamento
  • First Contentful Paint
  • Time to Interactive
  • Opportunità di ottimizzazione

Browser Dev Tools: Analisi Pratica

I browser dev tools offrono strumenti potenti per analizzare le performance. Vediamo i principali.

Elements Tab

L’Elements tab mostra quali elementi DOM vengono modificati:

  • Chrome evidenzia gli elementi che vengono toccati durante le operazioni
  • Utile per identificare re-rendering non necessari
  • Se eliminando un elemento vengono evidenziati tutti gli altri, probabilmente c’è un problema di ottimizzazione

Network Tab

Il Network tab mostra:

  • Dimensione dei file scaricati
  • Tempo di download
  • Numero di richieste HTTP
  • Possibilità di simulare connessioni lente per testare su reti reali

Throttling: è possibile simulare connessioni lente (es. Slow 3G) per vedere come si comporta l’applicazione su reti reali.

Performance Tab

Il Performance tab permette di:

  • Registrare snapshot: catturare un periodo di attività per analizzarlo
  • CPU throttling: simulare CPU più lente per testare su dispositivi meno potenti
  • Analisi dettagliata: vedere timeline delle operazioni, call stack, tempo di esecuzione

Come usarlo:

  1. Abilitare CPU throttling se necessario
  2. Cliccare “Record”
  3. Eseguire l’operazione da analizzare
  4. Fermare la registrazione
  5. Analizzare la timeline per identificare operazioni lente

Nella timeline si vedono:

  • Frames per second: FPS dell’applicazione
  • User interactions: eventi dell’utente (click, input, ecc.)
  • Main thread: il thread principale dove gira JavaScript
  • Call stack: stack delle chiamate di funzione

Operazioni che richiedono più di 50-100ms vengono spesso evidenziate in rosso come potenziali problemi.

Memory Tab

Il Memory tab permette di:

  • Prendere heap snapshot: snapshot dello stato della memoria
  • Confrontare snapshot: vedere differenze tra due stati della memoria
  • Identificare memory leaks: elementi che dovrebbero essere rimossi ma rimangono in memoria

Come identificare memory leaks:

  1. Prendere uno snapshot iniziale
  2. Eseguire operazioni (es. eliminare elementi)
  3. Prendere un altro snapshot
  4. Usare “Comparison” per vedere le differenze
  5. Cercare elementi “detached” che non dovrebbero essere più in memoria

Coverage Tab

Il Coverage tab (premere ESC nel Network tab) mostra:

  • Percentuale di codice JavaScript utilizzato
  • Codice utilizzato (linea verde) vs non utilizzato (linea rossa)
  • Opportunità per lazy loading: codice non utilizzato può essere caricato solo quando necessario

Audits Tab

Il Audits tab esegue controlli automatici:

  • Performance audit: analisi generale delle performance
  • First Contentful Paint: quando qualcosa appare sullo schermo
  • Time to Interactive: quando la pagina diventa interattiva
  • Opportunità di ottimizzazione: suggerimenti specifici

Ottimizzazioni Startup Time

Ridurre la Dimensione degli Script

La dimensione degli script è uno dei fattori più importanti per lo startup time:

  • Minificazione: webpack in modalità production minifica automaticamente il codice
  • Tree shaking: rimozione del codice non utilizzato
  • Code splitting: dividere il codice in chunk più piccoli caricati solo quando necessario

Evitare Troppi Round Trip

  • Bundling: usare webpack o strumenti simili per combinare file multipli
  • Evitare CDN per librerie: importare librerie nel bundle invece di usare CDN per permettere code splitting

Lazy Loading

Il lazy loading permette di caricare codice solo quando necessario:

// Invece di import statico
import { addProduct } from './product-management.js';
// Usare import dinamico
const handleAddProduct = async (event) => {
event.preventDefault();
const module = await import('./product-management.js');
module.addProduct(event);
};

Quando usare lazy loading:

  • Funzioni che vengono eseguite solo su azioni dell’utente
  • Codice che non è necessario al caricamento iniziale
  • Chunk di codice grandi che possono essere caricati on-demand

Considerazioni:

  • Il lazy loading aggiunge overhead (codice per gestire il caricamento dinamico)
  • Per applicazioni piccole potrebbe non essere conveniente
  • Per applicazioni grandi con molto codice opzionale, può fare una grande differenza

Code Splitting

Il code splitting va di pari passo con il lazy loading:

  • Webpack crea automaticamente chunk separati per import dinamici
  • Ogni chunk viene scaricato solo quando necessario
  • Riduce la dimensione del bundle iniziale

Esempio pratico:

Se abbiamo funzioni addProduct e deleteProduct che vengono usate solo su click, possiamo:

  1. Separare il codice in file dedicati
  2. Usare import dinamici solo quando necessario
  3. Ridurre il codice iniziale scaricato

Ottimizzazioni Runtime Performance

Manipolazione DOM Efficiente

Una delle ottimizzazioni più importanti riguarda la manipolazione del DOM.

Problema: Re-rendering Completo

Un errore comune è re-renderizzare l’intera lista quando si modifica un solo elemento:

// ❌ Non ottimale: re-renderizza tutto
function renderProducts(products) {
const productListEl = document.getElementById('product-list');
productListEl.innerHTML = ''; // Cancella tutto
products.forEach(product => {
// Crea tutti gli elementi da zero
const li = document.createElement('li');
// ... crea tutti gli elementi
productListEl.appendChild(li);
});
}

Problemi:

  • Cancella e ricrea tutti gli elementi anche se solo uno è cambiato
  • Operazioni DOM costose moltiplicate per ogni elemento
  • Su liste grandi o dispositivi lenti, diventa evidente

Soluzione: Update Selettivo

Invece di re-renderizzare tutto, aggiornare solo gli elementi necessari:

// ✅ Ottimale: aggiorna solo ciò che serve
function updateProducts(product, prodId, deleteProductFn, isAdding) {
const productListEl = document.getElementById('product-list');
if (isAdding) {
// Crea solo il nuovo elemento
const newProductEl = createProductElement(product, prodId, deleteProductFn);
productListEl.insertAdjacentElement('afterbegin', newProductEl);
} else {
// Rimuovi solo l'elemento specifico
const productEl = document.getElementById(prodId);
productEl.remove();
}
}

Vantaggi:

  • Solo gli elementi modificati vengono toccati
  • Altri elementi rimangono intatti
  • Performance molto migliori, specialmente su liste grandi

Best Practices DOM

  1. Batch operations: raggruppare modifiche DOM quando possibile
  2. Document fragments: usare fragment per inserire più elementi in una volta
  3. Evitare query ripetute: salvare riferimenti a elementi DOM invece di cercarli ogni volta
  4. Usare ID per accesso diretto: getElementById è più veloce di querySelector
// ✅ Salvare riferimenti invece di cercare ogni volta
const titleEl = document.getElementById('title');
const priceEl = document.getElementById('price');
// ❌ Evitare query ripetute
function addProduct() {
const title = document.getElementById('title').value; // Query ogni volta
}

Creazione Elementi DOM Efficiente

Quando si creano elementi DOM, ci sono approcci più efficienti:

// ✅ Usare innerHTML per contenuto semplice
function createProductElement(product, prodId, deleteProductFn) {
const li = document.createElement('li');
li.id = prodId;
li.innerHTML = `
<h2>${product.title}</h2>
<p>${product.price}</p>
`;
const button = document.createElement('button');
button.textContent = 'Delete';
button.addEventListener('click', () => deleteProductFn(prodId));
li.appendChild(button);
return li;
}

Nota sulla sicurezza: quando si usa innerHTML con input utente, bisogna sempre sanitizzare per prevenire XSS attacks. In questo esempio non viene fatto per semplicità, ma in produzione è essenziale.

Ottimizzazione Operazioni Array

Anche le operazioni sugli array possono essere ottimizzate:

// ✅ Usare findIndex e splice invece di filter
function deleteProduct(prodId) {
const deletedProductIndex = products.findIndex(
prod => prod.id === prodId
);
if (deletedProductIndex !== -1) {
const deletedProduct = products[deletedProductIndex];
products.splice(deletedProductIndex, 1);
return deletedProduct;
}
}
// ❌ Meno efficiente: crea nuovo array
function deleteProduct(prodId) {
const updatedProducts = [];
products.forEach(prod => {
if (prod.id !== prodId) {
updatedProducts.push(prod);
}
});
products = updatedProducts; // Nuovo array in memoria
}

Vantaggi:

  • Modifica l’array esistente invece di crearne uno nuovo
  • Meno allocazione di memoria
  • Più semplice e leggibile

Memory Leaks

I memory leak si verificano quando oggetti che non dovrebbero più essere in memoria rimangono referenziati e quindi non possono essere garbage collected.

Come Identificare Memory Leaks

Usare il Memory tab dei browser dev tools:

  1. Prendere uno heap snapshot iniziale
  2. Eseguire operazioni che dovrebbero liberare memoria (es. eliminare elementi)
  3. Prendere un altro snapshot
  4. Usare “Comparison” per vedere le differenze
  5. Cercare elementi “detached” che non dovrebbero essere più presenti

Esempio di Memory Leak

Un esempio comune è mantenere riferimenti a elementi DOM rimossi:

// ❌ Memory leak: mantiene riferimenti a elementi rimossi
const renderedProducts = [];
function addProduct(product) {
const productEl = createProductElement(product);
renderedProducts.push(productEl); // Aggiunge riferimento
productListEl.appendChild(productEl);
}
function deleteProduct(prodId) {
const productEl = document.getElementById(prodId);
productEl.remove(); // Rimuove dal DOM
// Ma productEl è ancora in renderedProducts!
// Il browser non può garbage collectarlo perché c'è ancora un riferimento
}

Problema: quando un elemento DOM viene rimosso ma rimane referenziato da un array o oggetto JavaScript, il browser non può eliminarlo dalla memoria perché JavaScript mantiene ancora un riferimento.

Soluzione

Pulire i riferimenti quando si rimuovono elementi:

// ✅ Pulire i riferimenti
function deleteProduct(prodId) {
const productEl = document.getElementById(prodId);
const index = renderedProducts.indexOf(productEl);
if (index !== -1) {
renderedProducts.splice(index, 1); // Rimuove riferimento
}
productEl.remove(); // Ora può essere garbage collected
}

Alternativa migliore: se non è necessario tenere traccia degli elementi, evitare completamente di memorizzarli.

Pattern Comuni di Memory Leak

  1. Event listeners non rimossi: aggiungere listener senza rimuoverli quando non più necessari
  2. Closure che mantengono riferimenti: closure che mantengono riferimenti a oggetti grandi
  3. Cache che cresce indefinitamente: cache che non viene mai pulita
  4. Timers non cancellati: setInterval o setTimeout non cancellati

Micro-Ottimizzazioni

Le micro-ottimizzazioni sono ottimizzazioni di piccole parti del codice che possono offrire miglioramenti marginali.

Esempio: Loop Alternatives

Diverse alternative per iterare su array hanno performance diverse:

// Opzione 1: forEach
const startTime = performance.now();
products.forEach(product => {
// operazione
});
const endTime = performance.now();
console.log(`forEach: ${endTime - startTime}ms`);
// Opzione 2: for tradizionale
const startTime = performance.now();
for (let i = 0; i < products.length; i++) {
// operazione con products[i]
}
const endTime = performance.now();
console.log(`for: ${endTime - startTime}ms`);

Risultati tipici (su jsperf.com):

  • for tradizionale: più veloce
  • for...of: leggermente più lento
  • forEach: più lento

Quando Evitare Micro-Ottimizzazioni

Le micro-ottimizzazioni vanno usate con estrema cautela:

  1. Differenze minime: spesso le differenze sono nell’ordine di millisecondi o percentuali piccole
  2. Leggibilità vs Performance: codice più veloce può essere meno leggibile
  3. Performance non fissa: le performance cambiano tra browser e nel tempo
  4. Contesto specifico: la differenza conta solo se l’operazione viene eseguita migliaia di volte al secondo

Regola generale: se non stai eseguendo migliaia di operazioni al secondo, la differenza non sarà percepibile dall’utente. Preferisci codice leggibile e manutenibile.

Quando Considerare Micro-Ottimizzazioni

Le micro-ottimizzazioni possono essere utili in scenari specifici:

  • Operazioni ad alta frequenza: loop che girano migliaia di volte al secondo
  • Applicazioni molto grandi: dove piccole ottimizzazioni si sommano
  • Dispositivi limitati: dove ogni millisecondo conta

Sempre misurare prima: usare strumenti come jsperf.com o performance.now() per verificare che l’ottimizzazione offra realmente un beneficio.


Ottimizzazioni Server-Side

Le performance non riguardano solo il codice JavaScript client-side. Anche la configurazione del server può influenzare significativamente le performance.

Compressione

La compressione riduce la dimensione dei file serviti:

  • File statici (CSS, JS, immagini) vengono compressi (zippati) prima dell’invio
  • I browser moderni sanno decomprimere automaticamente
  • Meno dati trasferiti = tempi di caricamento più veloci

Come configurarlo:

Caching

Il caching permette di riutilizzare dati o file già scaricati:

Browser caching:

  • Il browser memorizza file già scaricati (JS, CSS, immagini)
  • Controllato tramite header HTTP Cache-Control
  • Riduce richieste ripetute per gli stessi file

Server-side caching:

  • Memorizzazione di dati elaborati sul server (es. risultati query database)
  • Risposte più veloci per richieste multiple degli stessi dati
  • Riduce carico sul database

Risorse:

HTTP/2

HTTP/2 è la versione più recente del protocollo HTTP:

  • Server push: il server può inviare file attivamente al client invece di aspettare richieste
  • Miglior gestione di connessioni multiple
  • Header compressi

Risorse:


Runtime Performance

Da fare:

  • Evitare esecuzioni di codice non necessarie
  • Raggruppare operazioni quando possibile
  • Evitare accessi DOM non necessari
  • Trovare e fixare memory leaks, anche piccoli
  • Per operazioni ad alta frequenza, confrontare approcci alternativi

Da evitare:

  • Micro-ottimizzazioni premature senza misurazione
  • Ottimizzazioni che compromettono la leggibilità del codice
  • Assumere che performance siano fisse nel tempo

Startup Performance

Da fare:

  • Eliminare codice non utilizzato
  • Scaricare il pacchetto di codice più piccolo possibile inizialmente
  • Evitare troppe librerie third-party
  • Usare bundling e code splitting con lazy loading
  • Minificare il codice (webpack lo fa automaticamente in production)

Da evitare:

  • Usare CDN per librerie quando si può fare code splitting
  • Caricare tutto il codice all’avvio quando parte può essere lazy-loaded

Approccio Generale

  1. Misurare prima di ottimizzare: identificare i colli di bottiglia reali
  2. Focus sulle ottimizzazioni importanti: DOM manipulation e memory leaks prima delle micro-ottimizzazioni
  3. Testare su codice production: non misurare in modalità sviluppo
  4. Non esagerare: codice leggibile è spesso più importante di micro-ottimizzazioni
  5. Tenere aggiornati: le performance cambiano nel tempo, nuove feature JavaScript possono diventare più veloci

Risorse Aggiuntive

Documentazione Chrome DevTools

Web Performance

Strumenti

  • jsperf.com: benchmark di codice JavaScript
  • webpagetest.org: test performance di siti hostati
  • Chrome DevTools: strumenti integrati nel browser

Conclusione

L’ottimizzazione delle performance è un argomento vasto e continuo. Non esiste una soluzione universale: ogni applicazione ha esigenze specifiche.

Gli aspetti più importanti da considerare sono:

  • Manipolazione DOM efficiente: evitare re-rendering non necessari
  • Prevenzione memory leaks: identificare e risolvere riferimenti non puliti
  • Startup time: ridurre dimensione e numero di file da scaricare
  • Misurazione: sempre misurare prima di ottimizzare

Ricorda: l’obiettivo è migliorare l’esperienza utente, non ottimizzare ogni singola riga di codice. Spesso codice più leggibile e manutenibile è preferibile a micro-ottimizzazioni che offrono benefici minimi.

Continua la lettura

Leggi il prossimo capitolo: "Testing Automatico"

Continua a leggere