Lazy loading e code splitting sono due tecniche che risolvono lo stesso problema: non scaricare JavaScript che l'utente non usa subito. Il code splitting divide il bundle in chunk per route o componente; il lazy loading carica quei chunk solo quando servono. Insieme possono ridurre il JavaScript iniziale del 60-80%, abbassando drasticamente il Time to Interactive (TTI) e migliorando INP.
Perché il JavaScript è il collo di bottiglia più sottovalutato
Un'immagine pesante rallenta solo il suo download. Il JavaScript invece deve essere: scaricato, decompresso, parsato dal motore JS, compilato, ed eseguito — tutto sul main thread. Su un dispositivo mid-range (il mercato prevalente in Italia) processare 500 KB di JavaScript può bloccare il thread per 3-5 secondi, rendendo la pagina completamente non interattiva. Questa è la differenza tra un punteggio di performance "buono" su MacBook e "pessimo" per l'utente reale.
I dati che raccogliamo mostrano che i siti React/Next.js non ottimizzati partono spesso con bundle JavaScript iniziali di 800 KB-1,5 MB. Questo include dipendenze di tutte le pagine, componenti mai usati nella homepage, e librerie di terze parti caricate integralmente. Con code splitting e lazy loading sistematici, questi bundle scendono tipicamente a 150-250 KB per la route principale — una riduzione che si sente immediatamente nelle metriche TTI e INP.
Code splitting: un bundle per ogni route
Con il code splitting il bundler (Webpack, Turbopack, Vite) divide il JavaScript in chunk separati invece di produrre un unico file monolitico. Ogni route del sito carica solo il suo chunk, più i moduli condivisi (vendor bundle). Un sito con 20 pagine non scarica il codice di tutte le 20 pagine alla prima visita: scarica solo quello della homepage. Next.js implementa il code splitting automatico per ogni file in `app/` o `pages/`, senza configurazione aggiuntiva.
Next.js va oltre il semplice code splitting per route: implementa anche il prefetching automatico dei chunk per le route linkate nella viewport. Quando l'utente vede un `<Link>` nella pagina corrente, Next.js scarica in background il JavaScript della destinazione (con priorità bassa, senza impattare le performance correnti). Il risultato: la navigazione tra pagine sembra istantanea. Questa è una delle differenze fondamentali rispetto a siti multi-page tradizionali.
Dynamic import: lazy loading dei componenti React
L'istruzione `import()` dinamica permette di caricare un modulo JavaScript solo quando viene effettivamente chiamata, non all'avvio. In React si usa con `React.lazy()` e `<Suspense>`, in Next.js con `next/dynamic`. Casi d'uso tipici: modale che si apre solo su clic, slider di immagini, mappa interattiva, chatbot widget. Questi componenti non servono al caricamento iniziale e possono tranquillamente aspettare.
Un pattern che usiamo sistematicamente nei nostri progetti Next.js: tutti i componenti below-the-fold vengono lazy-loaded con `next/dynamic`. La homepage carica immediatamente solo l'header e la sezione hero (above-the-fold), poi carica in modo progressivo le sezioni successive mentre l'utente scorre. Questo approccio riduce il JavaScript iniziale del 40-60% nelle homepage tipiche e migliora il LCP di 0,5-1,5 secondi.
next/dynamic: la versione Next.js del lazy loading
`next/dynamic` aggiunge funzionalità specifiche: l'opzione `ssr: false` esclude il componente dal rendering server-side (utile per componenti che usano API browser come `window`), e `loading` permette di mostrare un placeholder durante il caricamento. Nelle nostre realizzazioni di siti web usiamo `next/dynamic` sistematicamente per le mappe, i widget di terze parti, e i componenti below-the-fold con logica complessa.
Un caso d'uso frequente con `ssr: false`: le mappe interattive (Mapbox, Leaflet, Google Maps). Queste librerie usano API browser (`navigator.geolocation`, `window.devicePixelRatio`) non disponibili server-side e pesano 200-500 KB. Con `next/dynamic(() => import("./Map"), { ssr: false, loading: () => <MapSkeleton /> })` la mappa non blocca il rendering server-side, viene scaricata solo su client, e mostra un placeholder durante il caricamento. Risultato: zero impatto sul LCP.
Tree shaking: eliminare il codice mai usato
Il tree shaking è il processo con cui il bundler elimina automaticamente il codice esportato ma mai importato. Funziona solo con ES modules (import/export), non con CommonJS (require). Un errore comune: importare intere librerie invece delle sole funzioni necessarie. `import _ from 'lodash'` include tutti i 70 KB di lodash; `import debounce from 'lodash/debounce'` include solo la funzione richiesta. La differenza è enorme sul bundle finale.
Le librerie di icone sono uno dei casi più comuni di bundle bloat da import errato. `import { FaHome } from 'react-icons/fa'` sembra innocuo, ma se la libreria non è ottimizzata per tree shaking può includere migliaia di icone nel bundle. La soluzione: usa librerie come `lucide-react` o `@heroicons/react` che hanno import ottimizzati per tree shaking, o genera gli SVG come componenti React locali per le icone che usi frequentemente.
Analizzare il bundle: trovare i moduli pesanti
Prima di ottimizzare, misura. `@next/bundle-analyzer` genera una mappa visuale del bundle in cui ogni rettangolo rappresenta un modulo, dimensionato proporzionalmente al suo peso. In pochi minuti scopri quali dipendenze occupano più spazio. Le sorprese più comuni: `moment.js` con tutti i locale (500 KB), librerie di icone importate integralmente, e SDK di terze parti non ottimizzati.
Nei bundle analyzer che analizziamo nei progetti dei clienti, le dipendenze pesanti più frequenti sono: `chart.js` o `recharts` importati integralmente quando serve solo un tipo di grafico (soluzione: usa `victory-chart` con import parziali), `date-fns` importato con `import * as dateFns` invece dei singoli metodi, e `@material-ui` o `antd` che portano decine di componenti inutilizzati. Sistemare questi tre punti porta spesso il vendor bundle da 800 KB a 300 KB.
Script di terze parti: caricali senza bloccare
Gli script di terze parti (Google Analytics, Meta Pixel, Hotjar, chat widget) sono spesso i peggiori offensori. In Next.js il componente `<Script>` di `next/script` permette di controllarli con la strategia `afterInteractive` (dopo TTI) o `lazyOnload` (durante l'idle). Non usare mai tag `<script>` nel markup direttamente: bloccano il parser HTML. Per la gestione dei siti web dei nostri clienti carichiamo tutti gli analytics con strategia `afterInteractive` per non penalizzare il LCP.
La strategia di caricamento degli script di terze parti ha un impatto diretto sul Total Blocking Time (TBT), che è il principale determinante del punteggio Lighthouse e un proxy per l'INP. Uno script Google Analytics caricato in modo sincrono aggiunge 100-200ms di TBT. Caricato con `afterInteractive` aggiunge 0ms al TBT (perché viene caricato dopo che la pagina è interattiva). La differenza nel punteggio Lighthouse può essere di 10-20 punti su mobile.
Misurare l'impatto: Coverage e Performance panel
Chrome DevTools offre due strumenti essenziali. Il tab "Coverage" (Ctrl+Shift+P → "Show Coverage") mostra quante righe di JavaScript vengono eseguite al caricamento: tutto ciò che è rosso non viene mai eseguito e può essere lazy-loaded. Il pannello "Performance" mostra il breakdown del main thread: le barre rosse ("Long Tasks") indicano blocchi superiori a 50 ms che causano jank e aumentano l'INP.
Un workflow pratico che usiamo negli audit: Coverage → identifica i moduli con > 60% codice non eseguito → sono candidati al lazy loading. Performance panel durante una navigazione completa (homepage + click su link interno) → identifica le long task durante la transizione → sono candidati per code splitting più granulare. Questo processo richiede circa 30 minuti per sito e porta sempre a ottimizzazioni concrete con impatto misurabile.
Risultati tipici del code splitting aggressivo
Dalla nostra esperienza sui progetti Next.js, l'implementazione sistematica di code splitting e lazy loading produce questi risultati tipici: JavaScript iniziale ridotto del 55-70%, TTI (Time to Interactive) migliorato di 1-3 secondi su mobile, INP ridotto del 40-60% grazie al main thread più libero, e punteggio Lighthouse mobile che sale di 15-25 punti. I risultati variano in base al punto di partenza, ma in nessun progetto abbiamo visto miglioramenti inferiori al 30% sul JavaScript iniziale dopo un'implementazione corretta.
Se gestisci un'agenzia SEO o un e-commerce e il tuo sito ha JavaScript iniziale sopra 300 KB, il code splitting è probabilmente il singolo intervento con il ROI più alto disponibile. Il nostro team può eseguire un bundle analysis gratuito e identificare i 3-5 interventi con maggiore impatto: contattaci per iniziare.




