next/image vs srcset vs CDN: ostateczny poradnik optymalizacji obrazów w Next.js

Opublikowano
10 kwietnia 2026
Aktualizacja
26 czerwca 2026
Czas czytania
7 min czytania

Dlaczego obrazy tak mocno wpływają na wydajność?

Next.js oferuje komponent next/image, który automatyzuje większość optymalizacji. Nie zawsze jest jednak najlepszym wyborem. W tym artykule porównuję trzy podejścia: next/image, natywny HTML z srcset i dedykowane CDN do obrazów.

Komponent next/image: co robi pod maską

next/image to wrapper nad tagiem <img>, który automatycznie:

  • Konwertuje format: domyślnie serwuje WebP, a AVIF po dodaniu go do images.formats.
  • Skaluje rozmiar: generuje warianty dla różnych viewportów.
  • Lazy loading: ładuje obrazy dopiero, gdy są blisko viewportu.
  • Zapobiega CLS: wymaga podania width i height albo użycia fill w kontenerze o stałym formacie.
  • Blur placeholder: wyświetla rozmyty podgląd podczas ładowania, ale dla zdalnych obrazów zwykle wymaga własnego blurDataURL.

Podstawowe użycie

Code
import Image from 'next/image'
 
// Obraz statyczny z importu: automatyczne wymiary i blur placeholder
import heroImage from '@/public/images/hero.jpg'
 
export default function HeroSection() {
  return (
    <Image
      src={heroImage}
      alt="Hero banner strony StriveLab"
      preload // Gdy to jednoznaczny LCP image
      placeholder="blur" // Automatyczny blur z importu
      className="h-auto w-full"
    />
  )
}
Code
// Obraz dynamiczny z URL-a: wymiary wymagane
export default function ProductCard({ product }: { product: Product }) {
  return (
    <Image
      src={product.imageUrl}
      alt={product.name}
      width={400}
      height={300}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    />
  )
}

Prop sizes: podstawa prawidłowego skalowania

sizes informuje przeglądarkę, jaką szerokość będzie miał obraz na różnych viewportach. Przy obrazach responsywnych bez tej informacji przeglądarka zakłada zwykle 100vw, więc może pobrać wariant większy niż potrzebny, nawet jeśli obraz zajmuje przykładowo 1/3 strony.

Code
// Blog post: obraz pełnej szerokości na mobile, połowa na desktop
<Image
  src="/images/blog/post-cover.jpg"
  alt="Cover"
  width={1200}
  height={630}
  sizes="(max-width: 768px) 100vw, 50vw"
/>
 
// Siatka produktów: 100% na mobile, 50% na tablecie, 25% na desktop
<Image
  src={product.image}
  alt={product.name}
  width={400}
  height={400}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
/>

Prop fill: obrazy responsywne bez wymiarów

Gdy nie znasz wymiarów (np. zdjęcia z CMS), użyj fill z kontenerem o określonym aspect ratio:

Code
<div className="relative aspect-video">
  <Image
    src={post.coverImage}
    alt={post.title}
    fill
    className="rounded-lg object-cover"
    sizes="(max-width: 768px) 100vw, 60vw"
  />
</div>

Konfiguracja domen zewnętrznych

Aby next/image mógł optymalizować obrazy z zewnętrznych źródeł, musisz dodać je do konfiguracji, ale nie rób szerokiego wildcarda na całą domenę, jeśli obrazy leżą tylko w jednym katalogu.

Code
// next.config.ts
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/images/**',
        search: '',
      },
      {
        protocol: 'https',
        hostname: 'cdn.sanity.io',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        pathname: '/photo-**',
      },
    ],
    formats: ['image/avif', 'image/webp'],
    qualities: [50, 75, 90],
    minimumCacheTTL: 60 * 60 * 24 * 30,
  },
}
 
export default nextConfig

qualities ma znaczenie od Next.js 16. Bez allowlisty ktoś mógłby wymusić wiele wariantów tego samego obrazu przez parametry jakości. Ustal 2-4 wartości, których naprawdę używasz. minimumCacheTTL ogranicza koszt ponownej optymalizacji, ale nie traktuj go jak magicznego przyspieszacza. Pierwsze żądanie nowego wariantu nadal płaci koszt transformacji, a potem dopiero działa cache. Zbyt wysokie TTL utrudnia też podmianę obrazu, bo Next.js nie daje prostego mechanizmu ręcznego czyszczenia cache obrazów.

Cache, koszty i limity Image Optimization

next/image optymalizuje obrazy na żądanie, co jest bardzo wygodne wygodne, ale ma pewne konsekwencje o których warto pamiętać:

  • pierwsze wejście na nowy wariant może być wolniejsze, ponieważ serwer generuje obraz

  • każdy rozmiar, format i quality to osobny wariant do cache

  • szerokie sizes i przypadkowe quality potrafią pomnożyć liczbę wariantów

  • na platformach serverless lub managed hosting liczba optymalizacji może mieć koszt

  • przy bardzo dużej bibliotece zdjęć dedykowany image CDN bywa tańszy i stabilniejszy

Dlatego w projektach e-commerce i marketplace nie wystarczy użyć masowo next/image. Najpierw trzeba policzyć, ile wariantów realnie wygenerujesz: liczba obrazów razy liczba szerokości razy liczba formatów razy liczba jakości. Jeśli masz 30 000 zdjęć produktów, next/image bez dyscypliny w sizes, qualities i źródłach, odbije się to na kosztach.

Przykład ograniczenia wariantów:

Code
// next.config.ts
const nextConfig = {
  images: {
    deviceSizes: [360, 640, 768, 1024, 1280, 1536],
    imageSizes: [96, 160, 320],
    qualities: [50, 75],
    formats: ['image/webp'],
  },
}

W sytuacji, w której masz mało obrazów hero i znaczny ruch mobilny, AVIF może zmniejszyć transfer, ale koduje się wolniej. Jeśli masz tysiące rzadko odwiedzanych zdjęć produktowych, WebP może być bardziej rozsądnym domyślnym formatem.

Bezpieczne źródła obrazów

Najgorsza konfiguracja to hostname: '**' albo szerokie dopuszczenie całego zewnętrznego hosta bez ścieżki, ponieważ wtedy komponent obrazu zaczyna działać jak publiczny proxy optimizer. Bezpieczniejszy wariant ogranicza protokół, host, pathname i query:

Code
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/products/**',
        search: '',
      },
    ],
  },
}

Jeśli CMS generuje podpisane URL-e z query stringiem, zdecyduj, czy search ma być pusty, konkretny, czy pominięty. Pusty search: '' blokuje dowolne parametry, a pominięcie search dopuszcza dowolny query string, co bywa potrzebne, ale zwiększa powierzchnię nadużyć i liczbę wariantów cache.

Natywny <img> z srcset: kiedy wystarczy

Nie każdy projekt potrzebuje next/image. Jeśli masz statyczną stronę, kontrolujesz warianty obrazów i nie potrzebujesz automatycznej konwersji formatów, natywny HTML jest prostszy i daje pełną kontrolę.

Code
<picture>
  <!-- AVIF: najlżejszy format dla nowoczesnych przeglądarek -->
  <source
    type="image/avif"
    srcset="
      /images/hero-400.avif   400w,
      /images/hero-800.avif   800w,
      /images/hero-1200.avif 1200w
    "
    sizes="(max-width: 768px) 100vw, 50vw"
  />
  <!-- WebP: fallback dla starszych przeglądarek -->
  <source
    type="image/webp"
    srcset="
      /images/hero-400.webp   400w,
      /images/hero-800.webp   800w,
      /images/hero-1200.webp 1200w
    "
    sizes="(max-width: 768px) 100vw, 50vw"
  />
  <!-- JPEG: ostateczny fallback -->
  <img
    src="/images/hero-800.jpg"
    alt="Hero banner"
    width="1200"
    height="630"
    loading="lazy"
    decoding="async"
  />
</picture>

Kiedy wybrać natywny srcset

  • Static export (output: 'export'): next/image domyślnie wymaga serwera do optymalizacji, chyba że skonfigurujesz zewnętrzny loader.
  • Pełna kontrola nad wariantami: sam generujesz dokładne rozmiary przez skrypt.
  • Art direction: na mobile chcesz inny kadr niż na desktopie.
  • Minimalizm: nie chcesz dodatkowej warstwy abstrakcji.

srcset odpowiada za "który rozmiar tego samego obrazu pobrać”, a <picture> odpowiada na “który obraz pobrać”.

Code
<picture>
  <source
    media="(max-width: 767px)"
    srcset="/images/hero-mobile.avif 800w"
    type="image/avif"
  />
  <source
    media="(min-width: 768px)"
    srcset="/images/hero-desktop.avif 1600w"
    type="image/avif"
  />
  <img
    src="/images/hero-desktop.jpg"
    alt="Panel analityczny kampanii Google Ads na ekranie laptopa"
    width="1600"
    height="900"
    fetchpriority="high"
  />
</picture>

Tego nie zastąpisz samym sizes, ponieważ sizes nie zmienia kadru. Jeśli mobilny hero wymaga ciasnego portretowego kadru, a desktop szerokiej sceny, <picture> jest prostszym i bardziej przewidywalnym narzędziem.

CDN do obrazów: Cloudinary, Imgix, Cloudflare Images

Dedykowane do obrazów oferują transformacje w locie: skalowanie, kadrowanie, filtry, konwersję formatów bez generowania wariantów w build time.

Integracja z next/image przez custom loader

Code
// lib/cloudinary-loader.ts
export default function cloudinaryLoader({
  src,
  width,
  quality,
}: {
  src: string
  width: number
  quality?: number
}) {
  const params = [
    `w_${width}`,
    `q_${quality || 75}`,
    'f_auto', // Automatyczny format (WebP/AVIF)
    'c_limit', // Nie powiększaj ponad oryginał
  ]
 
  return `https://res.cloudinary.com/twoje-konto/image/upload/${params.join(',')}/${src}`
}
Code
// next.config.ts
const nextConfig = {
  images: {
    loader: 'custom',
    loaderFile: './lib/cloudinary-loader.ts',
  },
}
Code
// Użycie identyczne jak zwykły next/image
<Image
  src="blog/hero-nextjs.jpg" // Ścieżka w Cloudinary
  alt="Next.js hero"
  width={1200}
  height={630}
  sizes="100vw"
/>
// Wynikowy URL: https://res.cloudinary.com/.../w_640,q_75,f_auto,c_limit/blog/hero-nextjs.jpg

CDN to dobre rozwiązanie, jeśli źródło obrazów jest zmienne i duże: CMS, marketplace, e-commerce, profile użytkowników, galerie, feedy. Wtedy problemem nie jest samo LCP na jednej stronie, ale operacyjny koszt tysięcy transformacji, czyszczenia cache, kadrowania, watermarków, prywatnych assetów i wariantów dla wielu kanałów.

Przy CMS-ach masz trzy rozsądne warianty:

ŹródłoNajlepszy wybórDlaczego
lokalne grafiki marketingowestatyczny import + next/imageautomatyczne wymiary i blur, mało wariantów
CMS z własnym image CDN, np. Sanityloader albo natywne URL-e CDNCMS już umie transformować obrazy
e-commerce z tysiącami zdjęćdedykowany image CDNkoszty, cache i transformacje są poza serwerem Next.js
static export<picture> / srcset albo CDN loaderbrak serwera optymalizującego

Nie dubluj optymalizacji, bo jeśli Cloudinary albo Sanity już zwraca f_auto, q_auto i właściwą szerokość, przepuszczanie tego jeszcze przez domyślny optimizer Next.js nie ma sensu. W takiej architekturze użyj custom loadera albo natywnych URL-i CDN.

Porównanie trzech podejść

Kryteriumnext/imageNatywny srcsetCDN (Cloudinary/Imgix)
Konwersja formatuAutomatyczna wg konfiguracjiRęcznaAutomatyczna
SkalowanieAutomatyczneRęczne wariantyOn-the-fly
Lazy loadingWbudowaneloading="lazy"loading="lazy"
CLS preventionWymuszony width/heightRęczneRęczne
Blur placeholderAutomatyczny dla części importów statycznychRęczny (LQIP)Zależy od CDN
KosztZa serwer Next.jsZeroOpłata za CDN
Static exportWymaga custom loaderDziałaDziała
KontrolaŚredniaPełnaPełna
SetupMinimalnyPracochłonnyŚredni

Jak wybrać podejście

To może teraz najprostszy sposób podjęcie decyzji.

  1. Masz standardową aplikację Next.js z serwerem albo Vercel? Zacznij od next/image.
  2. Robisz output: 'export' bez zewnętrznego loadera? Wybierz <picture> i srcset.
  3. Masz tysiące obrazów z CMS-a, marketplace albo e-commerce? Wybierz image CDN lub loader pod CDN.
  4. Potrzebujesz różnych kadrów na mobile i desktopie? Użyj <picture>, nawet jeśli reszta obrazów idzie przez next/image.
  5. Masz jeden hero i kilka grafik marketingowych? Statyczne importy + next/image są najprostsze i zwykle wystarczają.
  6. Masz już zoptymalizowane URL-e z Sanity, Cloudinary albo Imgix? Nie optymalizuj ich drugi raz bez powodu.

Najlepsze praktyki niezależnie od podejścia

1. LCP image: preload, loading="eager" albo fetchPriority="high"

W Next.js 16 prop preload zastąpił przestarzały priority. Jeśli wiesz, który obraz jest , użyj preload, ale tylko na jednym obrazie above-the-fold. Nadmiarowy preload, np. cały rząd kart produktów, konkuruje z krytycznym CSS/JS i pogarsza LCP.

Gdy kandydat do LCP zależy od viewportu albo masz kilka podobnie ważnych obrazów, bezpieczniej użyć loading="eager" albo fetchPriority="high".

Code
<Image src={hero} alt="..." preload />
// lub natywnie:
// <img src="..." loading="eager" fetchpriority="high" />

2. Poprawny alt dla SEO i dostępności

Code
// Źle
<Image src={product.image} alt="zdjęcie" />
<Image src={product.image} alt="" />
 
// Dobrze
<Image src={product.image} alt="Buty do biegania Nike Air Max 90 w kolorze białym" />

3. Unikaj layout shift z aspect ratio

Code
/* CSS aspect ratio jako zabezpieczenie */
.image-container {
  aspect-ratio: 16 / 9;
  position: relative;
  overflow: hidden;
}

4. Mierz realny efekt

Po zmianie obrazów sprawdź trzy rzeczy:

  • czy LCP wskazuje właściwy element w PageSpeed Insights lub WebPageTest?
  • czy pobrany rozmiar obrazu odpowiada layoutowi, a nie pełnej szerokości ekranu?
  • czy CLS jest zerowy dzięki width/height, fill z aspect ratio albo stabilnemu kontenerowi?

W DevTools otwórz Network, włącz kolumny Resource Size i Content-Type, a potem porównaj mobile i desktop. Jeśli karta produktu o szerokości 280 px pobiera obraz 1200 px, problemem zwykle jest błędne sizes.

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

Często zadawane pytania

Czy next/image spowalnia build?

Nie. next/image optymalizuje obrazy on-demand (przy pierwszym żądaniu), nie w build time. Wygenerowane warianty są cachowane, więc kolejne żądania tego samego wariantu mogą dostać obraz z cache, dopóki wpis jest dostępny i nie wygasł.

AVIF często daje lepszą kompresję niż WebP przy podobnej jakości, ale kodowanie jest wolniejsze. W Next.js kolejność w images.formats ma znaczenie: jeśli ustawisz AVIF przed WebP, przeglądarka obsługująca oba formaty dostanie AVIF. Przy dużym ruchu sprawdź koszt pierwszego kodowania i cache, bo zysk transferowy może kosztować więcej CPU.

Domyślnie nie działa, bo wymaga serwera do optymalizacji. Rozwiązania: użyj custom loadera (Cloudinary, Imgix) albo ustaw unoptimized: true w konfiguracji, co wyłącza optymalizację, ale komponent nadal działa. Dla statycznych eksportów często prostszy jest natywny srcset z picture.

Propem preload, jeśli obraz jest jednoznacznym kandydatem na LCP i chcesz rozpocząć jego pobieranie już z head dokumentu. W Next.js 16 priority jest przestarzałe na rzecz preload. Nie łącz preload z loading ani fetchPriority. Jeśli kandydat do LCP zależy od viewportu albo masz kilka możliwych obrazów, częściej bezpieczniejsze będzie loading="eager" albo fetchPriority="high".

width i height ustalają proporcje i chronią przed CLS, a sizes mówi przeglądarce, jaką szerokość obraz realnie zajmie na różnych viewportach. Przy obrazach responsywnych bez sizes przeglądarka zakłada zwykle 100vw, więc może pobrać wariant większy niż faktycznie potrzebny.

qualities to allowlista dozwolonych wartości quality. W Next.js 16 jest wymagana, bo nieograniczone parametry jakości pozwalałyby generować zbyt wiele wariantów tego samego obrazu. Jeśli ustawisz qualities: [50, 75, 90], Next użyje najbliższej dozwolonej wartości, a bezpośrednie żądanie API z niedozwoloną jakością dostanie 400.

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 rozwija własne projekty side-projecty.

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

Skontaktuj się ze mną

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów