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
- Audit della pagina: identificare quanti round trip ci sono, quanto è grande lo script da scaricare
- Budget di performance: stabilire obiettivi (es. script sotto 100kb)
- Misurazione runtime: usare browser dev tools per identificare operazioni lente
- 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 misurareconst 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.
forvsforEach) - 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:
- Abilitare CPU throttling se necessario
- Cliccare “Record”
- Eseguire l’operazione da analizzare
- Fermare la registrazione
- 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:
- Prendere uno snapshot iniziale
- Eseguire operazioni (es. eliminare elementi)
- Prendere un altro snapshot
- Usare “Comparison” per vedere le differenze
- 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 staticoimport { addProduct } from './product-management.js';
// Usare import dinamicoconst 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:
- Separare il codice in file dedicati
- Usare import dinamici solo quando necessario
- 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 tuttofunction 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 servefunction 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
- Batch operations: raggruppare modifiche DOM quando possibile
- Document fragments: usare fragment per inserire più elementi in una volta
- Evitare query ripetute: salvare riferimenti a elementi DOM invece di cercarli ogni volta
- Usare ID per accesso diretto:
getElementByIdè più veloce diquerySelector
// ✅ Salvare riferimenti invece di cercare ogni voltaconst titleEl = document.getElementById('title');const priceEl = document.getElementById('price');
// ❌ Evitare query ripetutefunction 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 semplicefunction 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 filterfunction 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 arrayfunction 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:
- Prendere uno heap snapshot iniziale
- Eseguire operazioni che dovrebbero liberare memoria (es. eliminare elementi)
- Prendere un altro snapshot
- Usare “Comparison” per vedere le differenze
- 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 rimossiconst 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 riferimentifunction 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
- Event listeners non rimossi: aggiungere listener senza rimuoverli quando non più necessari
- Closure che mantengono riferimenti: closure che mantengono riferimenti a oggetti grandi
- Cache che cresce indefinitamente: cache che non viene mai pulita
- Timers non cancellati:
setIntervalosetTimeoutnon 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: forEachconst startTime = performance.now();products.forEach(product => { // operazione});const endTime = performance.now();console.log(`forEach: ${endTime - startTime}ms`);
// Opzione 2: for tradizionaleconst 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):
fortradizionale: più velocefor...of: leggermente più lentoforEach: più lento
Quando Evitare Micro-Ottimizzazioni
Le micro-ottimizzazioni vanno usate con estrema cautela:
- Differenze minime: spesso le differenze sono nell’ordine di millisecondi o percentuali piccole
- Leggibilità vs Performance: codice più veloce può essere meno leggibile
- Performance non fissa: le performance cambiano tra browser e nel tempo
- 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:
- Su Firebase: compressione automatica
- Su Node.js/Express: usare middleware come
compression(https://github.com/expressjs/compression)
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:
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
- https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching
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:
Best Practices e Riepilogo
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
- Misurare prima di ottimizzare: identificare i colli di bottiglia reali
- Focus sulle ottimizzazioni importanti: DOM manipulation e memory leaks prima delle micro-ottimizzazioni
- Testare su codice production: non misurare in modalità sviluppo
- Non esagerare: codice leggibile è spesso più importante di micro-ottimizzazioni
- Tenere aggiornati: le performance cambiano nel tempo, nuove feature JavaScript possono diventare più veloci
Risorse Aggiuntive
Documentazione Chrome DevTools
- Performance Analysis: https://developer.chrome.com/docs/devtools/performance/reference
- Memory Problems: https://developer.chrome.com/docs/devtools/memory-problems
- Rendering Performance: https://web.dev/articles/rendering-performance
Web Performance
- Optimizing Website Speed: https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency
- HTTP Caching: https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching
- HTTP/2: https://developers.google.com/web/fundamentals/performance/http2
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.