Reducing Next.js Bundle Size: playbook de productie 2026
GHID practic pentru a reduce Next.js bundle size in productie: RSC corect, imports fine-grained, dynamic import, reduceri pe librarii si bugete in CI.

Cand bundle-ul client trece de cateva sute de kB, risti LCP lent, TTI intarziat si conversii mai mici pe mobil. In productie, fiecare kB irosit costa: CDN, timp de procesare pe device-uri slabe, si suport pentru bug-uri care apar doar pe 3G.
Pe scurt: in 2026, Next.js cu App Router si React Server Components permite bundle-uri radical mai mici daca limitezi use client, folosesti dynamic imports pentru UI grele, eviti barrel exports, adopti librarii ESM tree-shake friendly si pui bugete stricte in CI. Mai jos gasesti playbook-ul folosit in proiecte reale, cu trade-off-uri, configuratii si capcane din productie.
Ce inseamna “bundle size” in Next.js in 2026
Next.js (App Router) separa logic: cod de server (RSC, route handlers, server actions) si cod de client (componente marcate cu use client, third-party scripts, orice atinge DOM sau state client). Doar codul client ajunge in bundle-ul livrat in browser.
Cateva realitati operationale:
- Client bundle-ul este per ruta. Next face code splitting, dar daca ai imports comune in layout-uri, ele se sharuiresc pe mai multe rute.
- Orice
use clientintr-un arbore forteaza toate componentele child sa fie client. Minimalizeaza suprafata client. - CommonJS (CJS) rupe adesea tree-shaking. Alege ESM unde poti (
lodash-es,date-fns,dayjs,graphql-request). - Barrel exports (
index.tscare re-exporta tot) pot anula optimizarile, tragand cod nefolosit in bundle. - Third-party scripts domina adesea costul. Incarca-le cu
next/scriptsi gandeste „consent-first” si „lazyOnload”.
O replica sec-ingenereasca: bundle-ul nu se micsoreaza rugandu-ne la gzip — ci prin mai putin JavaScript livrat clientului.
Arhitectura minimala pentru bundle mic
Modelul pe care il recomandam in proiecte Next.js medii-mari:
- App Router (
app/) cu RSC by default; tot fetching-ul in server (directfetchin RSC sau server actions). - Componente client doar pentru interactiuni reale (formular complex, editor, grafic interactiv). „Client wrapper” subtire in jurul unor nuclee grele, incarcate dinamic.
- Imports fine-grained pentru UI (icons, charts) si pentru utilitare (lodash-es, date-fns). Fara barrel exports in pachetele partajate.
- CSS fara runtime JS: CSS Modules + Tailwind JIT sau PostCSS. Evitam CSS-in-JS cu runtime pe client cand nu e strict necesar.
- Fonts cu
next/fontsi subsetting; imagini optimizate cunext/image(nu cresc bundle-ul JS, dar reduc transferul total si LCP). - Analiza continua:
@next/bundle-analyzer, bugete in CI si alerte la regressii.
Exemplu: RSC + wrapper client minimal + dynamic import
app/products/page.tsx (server component implicit):
// app/products/page.tsx
import { getProducts } from './server-actions';
import ProductsTable from './ProductsTable'; // RSC, server-rendered table shell
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('../../components/charts/SalesChart').then(m => m.SalesChart), {
ssr: false, // doar client
loading: () => <div style={{ height: 240 }}>Loading chart...</div>,
});
export default async function ProductsPage() {
const products = await getProducts(); // server fetch, nu ajunge in bundle
return (
<div>
<h1>Products</h1>
<ProductsTable products={products} />
{/* grafic interactiv izolat in client, fara a contamina restul arborelui */}
<Chart data={products.map(p => p.sales)} />
</div>
);
}
app/products/server-actions.ts:
// app/products/server-actions.ts
export async function getProducts() {
const res = await fetch(process.env.API_URL + '/products', { cache: 'no-store' });
if (!res.ok) throw new Error('Failed to load products');
return res.json() as Promise<Array<{ id: string; name: string; sales: number[] }>>;
}
components/charts/SalesChart.tsx (client doar unde e nevoie):
// components/charts/SalesChart.tsx
'use client';
import { useMemo } from 'react';
import { Line } from 'react-chartjs-2'; // librarie grea, incarcata doar aici pe cerere
export function SalesChart({ data }: { data: number[] }) {
const chartData = useMemo(() => ({
labels: data.map((_, i) => `D${i+1}`),
datasets: [{ label: 'Sales', data, borderColor: '#3b82f6' }],
}), [data]);
return <Line data={chartData} />;
}
next.config.js cu modularizare imports si analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
experimental: {
modularizeImports: {
lodash: { transform: 'lodash/{{member}}' },
'date-fns': { transform: 'date-fns/{{member}}' },
'@mui/icons-material': { transform: '@mui/icons-material/{{member}}' },
},
optimizePackageImports: ['date-fns', 'lodash-es'],
},
compiler: {
removeConsole: process.env.NODE_ENV === 'production' ? { exclude: ['error', 'warn'] } : false,
},
transpilePackages: ['@acme/ui'],
webpack: (config, { isServer }) => {
// Evita sa bagi accidente in bundle-ul client
if (!isServer) {
config.resolve.alias['@sentry/node'] = false;
config.resolve.alias['fs'] = false;
}
// Optional: elimina locale-urile Moment (daca inca folosesti moment)
const webpack = require('webpack');
config.plugins.push(new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ }));
return config;
},
});
Fonts si icons
// app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}
Pentru icon-uri, importa granular: import { Home } from 'lucide-react'; sau foloseste SVG inline/optimizat. Evita pachete care re-exporta mii de icon-uri printr-un singur barrel.
Tehnici concrete si trade-off-uri
Tabelul de mai jos acopera practici frecvente, cu plusuri si minusuri:
| Tehnica | Când | Avantaje | Dezavantaje | Note |
|---|---|---|---|---|
| RSC + fetch pe server | Default pentru pagini si listari | Zero fetch libs pe client, bundle mic | Interactiuni necesita wrapper use client | Limiteaza logica client la strictul necesar |
| Dynamic import pentru UI grele | Grafice, editoare, harti | Code splitting, incarcare la nevoie | Posibile sarituri vizuale la load | Foloseste fallback vizual si ssr:false daca DOM-only |
| ESM-only libs (lodash-es, date-fns) | Inlocuiri pentru CJS | Tree-shaking real | Uneori ecosistem mai fragil pe CJS | Testeaza import paths; evita barrel |
| CSS Modules/Tailwind JIT | Majoritatea UI-urilor | Fara runtime JS, CSS mic | Tailwind necesita disciplina claselor | Vezi si articolul nostru despre structura CSS aici |
| Minim third-party scripts | Analytics, chat, A/B | Timp de incarcare redus | Uneori cerinte de marketing | Incarca cu next/script si consens |
| Icons per-file, nu pachete monolit | UI iconografie | Bundle mic | Mai multe imports explicite | Automatizeaza generarea de imports |
| Librarii data-light (graphql-request, ky) | Fetch/GraphQL | Dimensiune mica | Functionalitate limitata | In RSC, prefera fetch si server actions |
Alte tactici practice:
- Evita
momentsau incarca doar locale necesare. Preferabildate-fnssaudayjs. - Nu re-exporta toate componentele din
@acme/uiprintr-unindex.ts. Pastreaza imports punctuale:import Button from '@acme/ui/button'. - Pentru formulare mari,
react-hook-formeste in general mai compact decat Formik. - Daca ai GraphQL pe client,
urqlsaugraphql-requestsunt mai suple decat un client complet cu cache normalizat. - Pentru editor rich text,
TipTapsauLexicalcu plugin-uri strict necesare; incarca editorul dinamic doar pe pagina care il foloseste.
Observabilitate si bugete in CI
Fara masurare, optimizarea ramane teorie. Propunem o schema simpla:
- Local: porneste analyzer-ul pentru a vedea graf-ul per ruta.
- CI: ruleaza build si verifica bugete per ruta. In caz de regressie, PR-ul esueaza.
- Sentry/Logs: marcheaza release-uri cu modificari de bundle si coreleaza cu RUM (Real User Monitoring).
Analyzer si bugete
# analiza locala a bundle-ului
ANALYZE=true next build
Foloseste size-limit sau bundlesize pentru a seta bugete aproximative pe fisiere generate in .next/static/chunks sau pe rapoarte agregate:
// package.json
{
"scripts": {
"build": "next build",
"analyze": "ANALYZE=true next build",
"size": "size-limit"
},
"size-limit": [
{ "path": ".next/static/chunks/app-*.js", "limit": "180 KB" },
{ "path": ".next/static/chunks/pages/products-*.js", "limit": "200 KB" }
]
}
In productie, masoara LCP/INP/TTFB cu RUM (Plausible, Vercel Web Analytics, sau o solutie self-host). Ajusteaza bugetele pe baza traficului mobil real.
Ce se strica in productie
Cateva defecte pe care le vedem recurent in audituri si proiecte migrate:
- Hydration mismatch: parti ale UI-ului randate diferit intre server si client. Apare cand importi librarii care se bazeaza pe
windowin RSC sau cand folosesti data non-determinista. Solutie: izoleaza in componente client si disable SSR pentru acele bucati. - CJS grele in bundle: ai adus
lodashcomplet printr-un import generic sau printr-un barrel. Verifica analyzer-ul si treci lalodash-escu imports pe functie. - Locale-uri implicite:
momentbaga implicit toate locale-urile daca nu le ignori. Ignora locale-urile si/sau migreaza ladayjs. - Icons all-in:
@mui/icons-materialimportat din root aduce o multime de glyph-uri. FolosestemodularizeImportssi imports punctuale. - CSS-in-JS runtime pe client: styled-components/emotion fara precompilare pot adauga zeci de kB si cost de runtime. Daca ramai pe ele, activeaza transform-urile Next, altfel treci la CSS Modules/Tailwind unde are sens.
- Library server-only trasa in client: ex.
@sentry/node,aws-sdk,fs. Asigura-te ca ai aliasfalsepe client inwebpacksi importuri conditionale doar pe server. - Third-party chat/analytics injectate
beforeInteractivepe toate rutele: muta-leafterInteractivesaulazyOnload, ideal incarcate doar pe paginile care chiar au nevoie si dupa consimtamant. - Barrel exports in pachete interne: par convenabile, dar ucid tree-shaking. Pastreaza exports la nivel de fisier/folder granular.
Costuri, ROI si plan de implementare
Reducerea bundle-ului este un proiect de igiena tehnica cu efect direct in KPI-uri de produs. Din experienta noastra ca builderi:
- Efort: pentru un app Next.js mediu (20–40 rute), un sprint de 1–2 saptamani acopera audit + schimbari cu impact mare (imports, dynamic, third-party), urmat de un sprint pentru curatenie (CSS, icons, fonts, budgets CI).
- Cost: echipa de 1–2 ingineri front-end seniori. Costul real depinde de cat de adanc e datoriile tehnice (CJS, barrel-uri, UI libs).
- ROI: timpi de incarcare mai buni pe mobil, scoruri Core Web Vitals mai predictibile, si costuri mai mici la suport. In funnel-uri cu trafic mobil semnificativ, efectul se vede de obicei in cresterea ratei de finalizare — masurabil prin A/B sau gradual rollout.
Plan incremental propus (minimizand riscurile):
- Saptamana 1: porneste analyzer, mapeaza top 5 rute grele, elimina CJS vizibile (lodash -> lodash-es, moment -> dayjs/date-fns), configureaza
modularizeImportssi alias pe client. - Saptamana 2: sparge componente grele in dynamic imports, muta fetch pe server si reduce
use clientdoar la interactiuni, aplicanext/fontsi optimizeaza icons. - Saptamana 3: adopta bugete in CI si RUM; regleaza strategia pentru third-party scripts cu
next/scriptsi consimtamant; curatenie CSS (elimina runtime JS). - Saptamana 4: review final, documenteaza patterns si adauga lint rules/PR templates care blocheaza barrel exports si imports din pachete „all-in”.
Daca ai nevoie de context pe structura CSS si de ce anumite alegeri cresc bundle-ul, am scris si despre asta cand am mutat structura Tailwind. Iar pentru disciplina bugetelor, abordarea este similara cu reducerea costurilor la servicii AI — pornesti de la masurare si impui limite — vezi si discutia noastra pragmatica despre costuri AI aici.
Checklist rapid (practic)
- Verifica
ANALYZE=true next build; identifica cele mai grele rute si module. - Inlocuieste CJS grele cu ESM si imports punctuale; elimina barrel exports.
- Pastreaza RSC by default; doar wrapper-e
use clientacolo unde e inevitabil. - Dinamizeaza grafice/editare/harti; evita
beforeInteractivepentru terte scripturi. - Foloseste
next/fontcu subsetting; icons granulari. - Seteaza bugete in CI si RUM in productie; blocheaza regressiile.
FAQ
De ce imi creste bundle-ul chiar daca am App Router si RSC?
Pentru ca ai prea multe componente marcate cu use client, folosesti barrel exports sau librarii CJS. RSC ajuta doar daca mentii granita client minima si imports granulare.
E suficient sa rulez gzip/brotli pe CDN?
Nu. Compresia ajuta transferul, dar nu reduce costul de parse/execute JS pe device. Tinta este mai putin cod trimis, nu doar comprimat.
Pot folosi in continuare styled-components sau emotion?
Da, dar cu transform-ul Next/SWC pentru a elimina runtime-ul pe cat posibil. Daca bundle-ul ramane mare, migreaza parti critice la CSS Modules/Tailwind.
Are sens sa dezactivez SSR pentru componente grele?
Uneori. Pentru grafice/harti pur interactive, ssr:false cu next/dynamic evita erorile si reduce complexitatea. Nu dezactiva SSR pe UI critic SEO.
Cum masor buget per ruta in CI?
Rularea next build produce chunks per ruta. Cu size-limit/scripts poti valida marimea fisierelor pattern app-*.js, pages-*.js. Alternativ, ruleaza analyzer si parseaza raportul JSON.
E o idee buna sa combin toate icon-urile intr-un sprite?
Pentru bundle JS, mai bine importa fiecare icon ca SVG sau din biblioteca cu imports punctuale. Sprite SVG poate fi util pentru caching, dar nu confunda cu reducerea JS.
Key takeaways
- RSC by default si
use clientdoar la nevoie taie cel mai mult din bundle. - Imports punctuale ESM si eliminarea barrel exports sunt castiguri rapide si sigure.
- Dynamic import pentru UI grea ofera control fin asupra costului pe ruta.
- Third-party scripts domina adesea; incarca-le tarziu si conditionat.
- Bugete in CI + RUM in productie previn regresiile si mentin disciplina.
Daca construiesti sau migrezi un produs Next.js si vrei un audit sau un plan de reducere a bundle-ului cu risc mic, scrie-ne. MTBYTE poate intra rapid, masura, prioritiza si implementa schimbari concrete. Contacteaza-ne pe /contact.
URMATORUL PAS
Ti-a placut abordarea?
Aplicam aceleasi principii in proiectele clientilor: AI, automatizari, produse care nu se sting dupa lansare.