Errori e fix

Bundle cresciuto dopo un solo import: come trovarlo e ridurlo

Un import può trascinare un intero pacchetto, oppure spostare un confine client e portare nel browser codice che prima restava sul server. Misura prima di cambiare.

In sintesi

  • Gli import da barrel file possono includere l'intero modulo: verifica con il bundle analyzer, non a occhio.
  • Un nuovo `"use client"` sposta nel browser tutto ciò che il file importa.
  • Misura sempre la build di produzione: il dev server non riflette il bundle finale.

Risposta breve: un import può trascinare un intero pacchetto, oppure spostare un confine client e portare nel browser codice che prima restava sul server. Misura il delta sulla build di produzione prima di cambiare qualsiasi cosa.

Perché un import pesa più del file che nomina

Il bundler non legge il tuo file, segue un grafo. Ogni import è un arco, e gli archi si aggiungono prima che il tree shaking possa togliere qualcosa: il taglio funziona solo su export ESM che nessuno usa, dentro pacchetti che dichiarano sideEffects: false. Un modulo CommonJS espone un oggetto costruito a runtime, quindi l'elenco degli export non è statico e il bundler non ha modo di sapere cosa è sicuro eliminare. Un import di una riga può valere cento moduli, e la riga non lo dice.

Come si misura

# Next.js: First Load JS per rotta
next build

# analisi dei moduli
ANALYZE=true next build

# Vite
npx vite-bundle-visualizer

Confronta la rotta prima e dopo l'import. Un delta del 30% su una pagina di contenuto è un segnale; un delta del 2% può essere rumore.

Dove guardare davvero

# pesa ogni chunk della build di produzione
npx source-map-explorer ".next/static/chunks/*.js"

# grafo dei moduli per Vite
npx vite-bundle-visualizer

Nel browser il passo equivalente è Coverage, in DevTools: mostra quanti byte scaricati sono stati eseguiti. Una percentuale bassa su un chunk nuovo significa che l'import ha trascinato rami che la pagina non tocca. Vale la pena guardare anche il pannello Network filtrato su JS, ordinato per dimensione trasferita: se il file più grosso è una data di localizzazione o un dizionario di icone, la causa è già trovata.

Le quattro cause

CausaEsempioCorrezione
barrel file`import { X } from "lib"` include tuttoimport profondo o plugin di ottimizzazione
CommonJSpacchetto senza ESMalternativa ESM o import dinamico
nuovo confine client`"use client"` su un file che importa moltosposta l'hook nella foglia
locale e dati`moment` con tutti i locale`date-fns` o locale espliciti

L'import profondo

// può includere l'intero set
import { Search, User, Bell } from "some-icon-lib";

// include solo i tre moduli
import Search from "some-icon-lib/icons/search";
import User from "some-icon-lib/icons/user";

Con un pacchetto ESM senza effetti collaterali il tree shaking fa già questo. Con un pacchetto CommonJS non può: il bundler non sa quali parti sono sicure da eliminare.

Il confine client

// client-wrapper.tsx
"use client";
import { heavyEditor } from "heavy-editor"; // finisce nel bundle client
export function Editor() {
  return heavyEditor();
}

Ogni direttiva "use client" porta nel browser il modulo e le sue dipendenze. Se il file importa una libreria grande per una sola funzione, quella libreria pesa sulla pagina anche se l'utente non la usa.

Quando la correzione non funziona

Se il bundle non scende dopo l'import profondo, il pacchetto ha effetti collaterali o è CommonJS. Controlla sideEffects nel package.json della libreria e il formato dei moduli distribuiti. Un secondo caso è la cache del bundler: cancella la cartella di build prima di misurare, altrimenti stai guardando il risultato precedente.

Quando la correzione tipica peggiora le cose

La correzione che si applica per prima è caricare la libreria in modo dinamico con dynamic() o React.lazy. Il numero nel report scende e il costo cambia posto. Il chunk non sparisce: si scarica al primo clic, quindi la prima interazione aspetta un giro di rete che prima non aspettava. Se il componente sta sopra la piega, hai aggiunto uno stato di caricamento e uno spostamento di layout a una pagina che prima si disegnava completa. La variante con ssr: false è peggiore: toglie il componente dalle statistiche della build, e il report sembra pulito mentre l'utente paga lo stesso.

Il limite della metrica

Un limite da dire: il byte non è il tempo. Su una connessione veloce un chunk da 40 KB arriva in pochi millisecondi, mentre un componente piccolo che idrata un albero profondo tiene occupato il thread principale e peggiora la reattività. Se l'obiettivo è la metrica di interazione, il numero da guardare è il tempo di esecuzione per chunk, non il peso trasferito: sono due misure diverse e la seconda non sostituisce la prima.

Come prevenirlo

  • Un budget di bundle per rotta, controllato in CI.
  • Nessun import da barrel file nelle pagine pubbliche.
  • Controlla il peso dopo ogni nuovo "use client".
  • Un commento di una riga accanto a ogni import pesante: chi lo aggiunge spiega perché, chi lo rimuove sa cosa rompe.

Dove continuare

Nel catalogo: componenti, dashboard e blocchi. Le guide: quanto pesa una libreria UI e hook in un Server Component; il confronto: copy-paste o headless.

Domande frequenti

Come misuro il bundle in Next.js?

L'output di `next build` mostra First Load JS per rotta. Per il dettaglio dei moduli serve `@next/bundle-analyzer`.

Perché le icone pesano così tanto?

Un import da barrel file può includere l'intero set. Gli import profondi, o un plugin di ottimizzazione, includono solo le icone usate.

`"use client"` aumenta sempre il bundle?

No, ma sposta nel client il modulo e le sue dipendenze. Se il file importa una libreria grande, quella libreria entra nel bundle.

Il tree shaking non dovrebbe risolvere?

Funziona con moduli ESM e codice senza effetti collaterali. Un pacchetto CommonJS o con side effect dichiarati non viene ridotto.

Quanto è troppo?

Non c'è un numero universale. Confronta la rotta con le altre: un salto del 30% dopo un import merita una verifica.