Przejdź do treści

Jak analizować i zmniejszyć paczkę JS w Next.js?

Twój Next.js waży za dużo? Dowiedz się, jak za pomocą analizatora wykryć zbędne kilobajty JavaScriptu, usunąć ciężkie biblioteki i przyspieszyć stronę.

Maciej Sala

Founder StriveLab

6 min czytaniaOpublikowano 11 kwietnia 2026 (Aktualizacja 17 lipca 2026)

Dlaczego rozmiar pakietu JavaScriptu w Next.js ma znaczenie?

Next.js automatycznie dzieli kod na osobne pakiety per route (), ale nie zapewni to ochrony przed importowaniem całego lodash zamiast jednej funkcji, ciężkimi bibliotekami UI w każdym Client Component i zależnościami, które powinny zostać na serwerze.

Zacznij od granicy 'use client'

Dyrektywa 'use client' wyznacza granicę między grafem modułów serwera i klienta. Wszystko, co importuje Client Component, może trafić do paczki klienta. Dlatego nie oznaczaj nią całego layoutu lub strony tylko dlatego, że zawierają jeden interaktywny element. Zostaw stronę jako Server Component, a 'use client' umieść w małym komponencie formularza, wyszukiwarki czy przycisku.

Krok 1: zmierz pakiet JavaScriptu wbudowanym analizatorem Next.js

W Next.js 16.1+ analizator Turbopacka pozwala filtrować moduły po trasie i środowisku (klient lub serwer), a także prześledzić pełny łańcuch importów. To najlepszy punkt startu w projekcie korzystającym z domyślnego bundlera:

Code
npx next experimental-analyze

Do porównania raportu przed i po optymalizacji zapisz go na dysku:

Code
npx next experimental-analyze --output

Raport trafi do .next/diagnostics/analyze. Nie powstaje przy tym artefakt produkcyjnego buildu, więc standardowy npm run build nadal uruchom osobno przed wdrożeniem.

Alternatywa: @next/bundle-analyzer dla Webpacka

Jeśli projekt jest nadal budowany Webpackiem albo chcesz porównać jego raport, użyj wtyczki @next/bundle-analyzer:

Code
npm install -D @next/bundle-analyzer
Code
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {}
 
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})
 
module.exports = withBundleAnalyzer(nextConfig)
Code
# Uruchom build przez Webpack i otwórz raport
ANALYZE=true npm run build -- --webpack

Wtyczka otworzy osobne raporty dla pakietów klienta, serwera i edge runtime. Nie traktuj jednak wyniku Webpacka jako dokładnego odpowiednika buildu Turbopacka: do optymalizacji produkcyjnej analizuj przede wszystkim bundler używany przez projekt.

Czego szukać w analizie paczek kodu Next.js?

Duże biblioteki w pakiecie klienta

Typowi winowajcy:

Biblioteka lub typ koduCo sprawdzić w raporcieTypowa decyzja
moment.jsCzy potrzebujesz pełnej obsługi dat i lokalizacji?Użyj mniejszej biblioteki lub tylko potrzebnych funkcji.
lodashCzy importujesz cały pakiet?Importuj pojedynczy moduł, użyj lodash-es albo natywnego API.
framer-motionCzy animacja jest potrzebna przy pierwszym renderze?Użyj CSS lub załaduj komponent warunkowo.
Biblioteka wykresówCzy wykres pojawia się na każdej trasie?Ładuj go dynamicznie tylko na trasie lub po interakcji.
Biblioteka komponentówCzy importy są zoptymalizowane i które komponenty są klientowe?Korzystaj z nazwanych importów i sprawdź wynik w mapie drzewa.

Nie zakładaj, że zamiana jednej biblioteki na drugą automatycznie zmniejszy wynik. Rozmiar zależy od wersji, sposobu importu, użytych funkcji i pozostałych zależności. Zawsze porównaj raport przed i po zmianie.

Biblioteki serwerowe przypadkiem wrzucone do pakietu klienta

To najgroźniejszy przypadek, ponieważ jeśli w pakiecie klienta widzisz prisma, bcrypt, stripe albo @aws-sdk, kod serwerowy przedostał się do grafu klienta. Część takich importów zakończy build błędem, a inne niepotrzebnie zwiększą paczkę lub ujawnią nieprzeznaczony dla przeglądarki kod. Zwykle chodzi o jeden plik, importowany i przez Server, i przez Client Component.

Code
// ŹLE — jeden plik miesza kod klienta i serwera
// utils.ts
import bcrypt from 'bcrypt'
 
export function formatDate(date: Date) {
  return new Intl.DateTimeFormat('pl-PL').format(date)
}
 
export function hashPassword(password: string) {
  return bcrypt.hash(password, 12)
}

Rozwiązanie: rozdziel na osobne pliki:

Code
lib/
├── utils-client.ts    ← formatDate, formatPrice
└── utils-server.ts    ← hashPassword, db queries

Lub użyj server-only package:

Code
npm install server-only
Code
// lib/utils-server.ts
import 'server-only' // Błąd, jeśli moduł zaimportuje Client Component
import bcrypt from 'bcrypt'
 
export function hashPassword(password: string) {
  return bcrypt.hash(password, 12)
}

Krok 2: optymalizacja pakietu JavaScriptu w Next.js

Dynamiczny import i leniwe ładowanie ciężkich komponentów

Code
'use client'
 
import { useState } from 'react'
import dynamic from 'next/dynamic'
 
const CodeEditor = dynamic(() => import('@/components/code-editor'), {
  ssr: false,
  loading: () => <p>Ładowanie edytora...</p>,
})
 
export function EditorPanel() {
  const [isEditorOpen, setIsEditorOpen] = useState(false)
 
  return (
    <section>
      <button onClick={() => setIsEditorOpen(true)}>Edytuj kod</button>
      {isEditorOpen && <CodeEditor />}
    </section>
  )
}

dynamic() wydziela komponent do osobnego pliku JavaScriptu. Żeby odroczyć jego pobranie, renderuj go dopiero wtedy, gdy użytkownik go potrzebuje, jak w przykładzie po kliknięciu. Samo ssr: false wyłącza renderowanie na serwerze, ale nie oznacza automatycznie ładowania po interakcji.

Tree shaking: importuj tylko potrzebny kod

usuwa z pakietu kod, którego nie używasz — ale tylko wtedy, gdy importujesz w sposób, który mu na to pozwala.

Code
// ŹLE — importuje cały lodash
import _ from 'lodash'
_.debounce(fn, 300)
 
// DOBRZE — importuje jedną funkcję
import debounce from 'lodash/debounce'
debounce(fn, 300)
 
// NAJLEPIEJ — natywny JS, gdy naprawdę wystarcza prosty debounce
function debounce<TArgs extends unknown[]>(
  fn: (...args: TArgs) => void,
  ms: number,
) {
  let timer: ReturnType<typeof setTimeout>
  return (...args: TArgs) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), ms)
  }
}
Code
// ŹLE — importuje cały pakiet ikon
import * as Icons from 'lucide-react'
 
// DOBRZE — importuje jedną ikonę
import { Search } from 'lucide-react'

Next.js ma tu wbudowanego, eksperymentalnego pomocnika: optimizePackageImports. Dla wskazanych pakietów kompilator przepisuje import z barrela na bezpośrednie importy plików, więc nazwany import z ogromnej biblioteki przestaje ryzykować wciągnięciem całości. Popularne pakiety (m.in. lucide-react, date-fns, lodash-es, @mui/icons-material) są optymalizowane automatycznie — własne dopisujesz w konfiguracji:

Code
// next.config.ts
const nextConfig = {
  experimental: {
    optimizePackageImports: ['moja-biblioteka-ui'],
  },
}

Barrel files we własnym kodzie: sprawdź, czy nie utrudniają tree shakingu

Barrel file — index.ts re-eksportujący wszystko z katalogu — daje krótkie importy. Przy czystych modułach ESM bundler zwykle usunie nieużywane eksporty. Sprawdź go jednak, gdy mapa drzewa pokazuje kod niepotrzebny na danej trasie: efekty uboczne, CommonJS lub nieoptymalny graf importów mogą uniemożliwić bezpieczne wycięcie modułu.

Code
// components/index.ts — wygodny barrel
export { Button } from './button'
export { Card } from './card'
export { RevenueChart } from './revenue-chart' // ciężki: recharts w środku
export { PdfPreview } from './pdf-preview' // ciężki: pdf.js w środku
Code
// Strona kontaktu potrzebuje tylko przycisku...
import { Button } from '@/components'
// Jeśli którykolwiek moduł ma efekty uboczne, bundler może nie usunąć
// RevenueChart i PdfPreview z paczki klienta.

optimizePackageImports działa tylko dla pakietów z node_modules — własnych barreli nie zmienia. Reguła praktyczna: gdy ciężka zależność pojawia się na trasie, która jej nie używa, zaimportuj komponent bezpośrednio z pliku i porównaj raport. Nie zakładaj jednak, że każdy barrel jest błędem: decyduje wynik analizy, nie sam styl importu.

Jak sprawdzić rozmiar zależności przed instalacją?

Zanim pakiet trafi do package.json, sprawdź, ile waży: bundlephobia.com pokazuje rozmiar po minifikacji i gzipie, koszt per wersja i to, czy pakiet wspiera tree shaking. Jak dla mnie, najwygodniejsze jest rozszerzenie VS Code Import Cost, które wyświetla rozmiar każdego importu bezpośrednio w edytorze. Dzięki temu ciężkie zależności zauważysz już w momencie ich dodawania, zamiast wykrywać je z opóźnieniem podczas analizy drzewa paczki.

Server Components i redukcja JavaScriptu w pakiecie klienta

W ramach optymalizacji, najlepiej przenieść logikę z Client na Server Components. Własny kod Server Component i używane przez niego biblioteki nie są wysyłane jako JavaScript do hydratacji, choć odpowiedź nadal zawiera wyrenderowany HTML i dane RSC potrzebne Reactowi.

Code
// app/products/product-list.client.tsx
// PRZED — Client Component z useEffect (dodaje JS do paczki klienta)
'use client'
import { useState, useEffect } from 'react'
 
type Product = { id: string; name: string }
 
function ProductList() {
  const [products, setProducts] = useState<Product[]>([])
  useEffect(() => {
    fetch('https://api.example.com/products')
      .then((r) => r.json())
      .then(setProducts)
  }, [])
  return products.map((p) => <div key={p.id}>{p.name}</div>)
}
Code
// app/products/product-list.tsx
import { getProducts } from '@/lib/products'
 
// PO — Server Component (bez własnego JavaScriptu do hydratacji)
export default async function ProductList() {
  const products = await getProducts()
  return products.map((p) => <div key={p.id}>{p.name}</div>)
}

Krok 3: pilnuj budżetu rozmiaru pakietu w CI/CD

Najpierw zapisz raport dla ważnych tras i ustal wartość bazową. Budżet powinien dotyczyć konkretnego artefaktu lub grupy plików, które chcesz kontrolować, a nie przypadkowego rozmiaru całego katalogu .next.

Przykład z size-limit dla wygenerowanych plików klienta:

Code
npm install -D size-limit @size-limit/file
Code
// package.json
{
  "scripts": {
    "size": "npm run build && size-limit"
  },
  "size-limit": [
    {
      "name": "client chunks",
      "path": ".next/static/chunks/**/*.js",
      "limit": "200 kB"
    }
  ]
}

Dodaj ten skrypt do pipeline'u CI/CD:

Code
# .github/workflows/bundle-check.yml
- name: Build and check bundle
  run: npm run size

size-limit zakończy zadanie błędem po przekroczeniu limitu. Jeśli potrzebujesz kontroli konkretnych tras lub różnicy względem gałęzi bazowej, użyj raportu analizatora, bundlewatch albo narzędzia dopasowanego do platformy wdrożeniowej.

Jak oceniać wynik analizy?

Nie porównuj tras tylko po jednej liczbie kilobajtów. W raporcie odpowiedz kolejno na trzy pytania:

  1. Czy ciężki moduł jest w paczce klienta, choć mógł pozostać na serwerze?
  2. Czy jest potrzebny przy pierwszym renderze, czy można załadować go po interakcji?
  3. Który import wprowadza go na trasę?

Po zmianie wygeneruj drugi raport i sprawdź rzeczywisty efekt w Lighthouse lub na danych RUM. Analiza pakietu pokazuje przyczynę, ale nie zastępuje pomiaru czasu ładowania, interaktywności i Core Web Vitals na urządzeniach użytkowników.

Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Jaki rozmiar pakietu JavaScriptu jest akceptowalny?

Nie ma jednej wartości dobrej dla każdej strony. Ustal budżet osobno dla najważniejszych tras i porównuj kolejne raporty z własną wartością bazową. Ważniejszy od pojedynczej liczby jest trend, rzeczywisty koszt JavaScriptu na urządzeniach docelowych i brak regresji Core Web Vitals. W Next.js 16 next build nie pokazuje już metryki First Load JS, dlatego do pomiaru użyj analizatora pakietów, a efekt sprawdź także w Lighthouse lub na danych RUM.

Czy Server Components naprawdę nie dodają JavaScriptu?

Tak. Server Components renderują się na serwerze i wysyłają do przeglądarki gotowy HTML, więc ich własny kod nie trafia do pakietu klienta. Jest jednak ważny wyjątek: Client Components importowane wewnątrz Server Component już do pakietu klienta trafiają. Dlatego przeniesienie logiki na serwer działa tylko wtedy, gdy faktycznie nie wymaga ona interaktywności po stronie klienta.

Jak znaleźć, skąd pochodzi ciężka zależność w pakiecie klienta?

Najszybciej w mapie drzewa z next experimental-analyze albo, w projekcie opartym na Webpacku, z @next/bundle-analyzer. Kliknięcie w duży blok pokazuje ścieżkę modułu i to, co dany pakiet ze sobą ciągnie. Gdy chcesz prześledzić, dlaczego pakiet w ogóle jest w projekcie, uruchom npm ls <pakiet> (lub pnpm why <pakiet>), wtedy dostaniesz cały łańcuch zależności prowadzący do instalacji. Potem zostaje grep po imporcie w kodzie, bo do pakietu klienta trafia zawsze przez jakiś konkretny import w Client Component.

Czym są barrel files i czemu niepotrzebnie powiększają pakiet JavaScriptu?

Barrel file to index.ts, który re-eksportuje wszystko z katalogu, żeby importy były krótsze. Sam barrel nie musi powiększać paczki: bundler może usunąć nieużywane eksporty. Ryzyko pojawia się, gdy re-eksportowane moduły mają efekty uboczne, używają CommonJS albo nie mogą zostać bezpiecznie wycięte. Ciężkie komponenty importuj wtedy bezpośrednio z pliku, a dla popularnych bibliotek użyj optimizePackageImports.

Czym jest tree shaking i kiedy nie działa?

Tree shaking to eliminacja nieużywanego kodu podczas budowania. Bundler usuwa funkcje, których nie importujesz. Działa najlepiej z importami nazwanymi z pakietów modułowych (ESM), np. import debounce from 'lodash/debounce' zamiast import _ from 'lodash'. Zawodzi przy bibliotekach w starym formacie CommonJS, przy imporcie całego pakietu (import *) oraz gdy moduł ma efekty uboczne, których bundler nie może bezpiecznie usunąć.

Czy warto pilnować rozmiaru pakietu JavaScriptu w CI?

Tak, bo bez automatycznej kontroli pakiet rośnie niezauważalnie i każdy dokłada „tylko jedną małą bibliotekę", aż strona zaczyna się wolno ładować. Narzędzia takie jak size-limit czy bundlewatch mogą przerwać build po przekroczeniu ustalonego progu, więc regresja zostaje wyłapana w PR-ze, a nie na produkcji. Próg ustaw względem zmierzonej wartości bazowej dla konkretnego artefaktu lub trasy i uzupełnij go kontrolą Core Web Vitals.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Jak React Server Components wpływają na SEO i performance?

Przez lata React pchał, coraz więcej i więcej pracy do przeglądarki, w efekcie czego mamy większe bundle, hydratacja trwa dłużej, są puste loadingi i strony, które bez JavaScriptu straciły sens. App Router odwraca ten kierunek. React Server Components pozwalają renderować treść na serwerze bez wysyłania całej logiki do klienta, a Server Actions upraszczają formularze i mutacje. To ma znaczenie dla SEO, bo crawler szybciej dostaje HTML. Ma też znaczenie dla biznesu, bo użytkownik szybciej widzi i dostaje to, po co przyszedł.

Maciej Sala

Maciej Sala

Founder StriveLab

Astro vs Next.js w 2026: Porównanie frameworków

Astro czy Next.js? Wybór frameworka musi być dokładnie przemyślany, zanim pojawi się pierwszy commit. Jeśli stoisz przed takim właśnie wyborem, w tym artykule staram się wykazać, w jakich obszarach najlepiej sprawdza się Astro , a w jakich będzie dominował Next.js .

Maciej Sala

Maciej Sala

Founder StriveLab

Jak osiągnąć 100/100 w Lighthouse Performance w Next.js? Case study

Mamy tu typową stronę usługową zbudowaną w Next.js, wyposażoną w Google Analytics, Google Tag Manager , prosty formularz kontaktowy, galerię zdjęć i animacje. Na początku testy PageSpeed Insights pokazały wyniki: 67/100 na urządzeniach mobilnych i 89/100 na komputerach stacjonarnych.

Maciej Sala

Maciej Sala

Founder StriveLab