Przejdź do treści

next/image vs srcset vs CDN: optymalizacja obrazów w Next.js

Porównanie next/image, srcset i image CDN. Popraw LCP, dobierz sizes, skonfiguruj cache i bezpiecznie obsłuż obrazy z CMS w Next.js 16.

Maciej Sala

Founder StriveLab

9 min czytaniaAktualizacja

Dlaczego obrazy wpływają na LCP, CLS i Core Web Vitals?

W przypadku obrazu LCP liczy się nie tylko rozmiar pliku. Przeglądarka musi wcześnie odkryć właściwy URL, nadać mu odpowiedni priorytet, pobrać zasób i wyrenderować go po zastosowaniu CSS. Obraz w HTML może zostać znaleziony przez preload scanner wcześniej niż tło zapisane w zewnętrznym CSS. Z kolei błędny sizes może sprawić, że poprawnie odkryty obraz nadal będzie niepotrzebnie ciężki.

CLS jest osobnym problemem. width i height nie określają renderowanego rozmiaru, ale przekazują przeglądarce proporcje potrzebne do zarezerwowania miejsca. Dla fill tę odpowiedzialność przejmuje stabilny kontener. Next.js automatyzuje te elementy, lecz nadal wymaga poprawnej informacji o layoucie.

next/image w Next.js: 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 next/image

Code
import Image from 'next/image'
import heroImage from '@/public/images/hero.jpg'
 
type Product = {
  name: string
  imageUrl: string
}
 
// Statyczny import dostarcza wymiary i blurDataURL.
export function HeroSection() {
  return (
    <Image
      src={heroImage}
      alt="Panel analityczny StriveLab z wynikami kampanii"
      sizes="100vw"
      preload
      placeholder="blur"
      className="h-auto w-full"
    />
  )
}
 
// Dla zdalnego obrazu podaj proporcje oraz rzeczywisty layout w sizes i CSS.
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"
      className="h-auto w-full"
    />
  )
}

Prop sizes w next/image: podstawa prawidłowego skalowania

sizes informuje przeglądarkę, jaką szerokość będzie miał obraz na różnych viewportach. W next/image wpływa również na zestaw szerokości wygenerowanych w srcset. Bez tej informacji responsywny obraz może zostać potraktowany jak 100vw i pobrać wariant większy niż potrzebny.

Code
// Blog post: obraz pełnej szerokości na mobile, połowa na desktop
<Image
  src="/images/blog/post-cover.jpg"
  alt="Wykres porównujący czas ładowania formatów obrazów"
  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 w next/image: 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 dla next/image

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: '/products/**',
        search: '',
      },
    ],
    deviceSizes: [360, 640, 768, 1024, 1280, 1536],
    imageSizes: [96, 160, 320],
    formats: ['image/webp'],
    qualities: [50, 75],
    minimumCacheTTL: 60 * 60 * 24,
  },
}
 
export default nextConfig

W Next.js 16 domyślna allowlista jakości to [75]. Rozszerz ją tylko wtedy, gdy komponenty rzeczywiście przekazują inne quality. minimumCacheTTL jest dolną granicą: końcowy czas cache jest większą wartością z tej konfiguracji i nagłówka Cache-Control źródła. Next.js nie udostępnia prostego API do unieważniania cache, więc dla często zmienianych obrazów stosuj wersjonowany URL zamiast bardzo długiego TTL.

remotePatterns traktuj jak regułę bezpieczeństwa. Ogranicz protokół, host, ścieżkę i parametry zapytania. search: '' blokuje query string, a pominięcie search dopuszcza dowolne parametry i może zwiększyć liczbę wariantów cache. Domyślny optimizer nie przekazuje nagłówków uwierzytelniających do zdalnego źródła; prywatne obrazy wymagają podpisanego publicznego URL-a, własnego endpointu albo świadomego użycia unoptimized.

Cache, koszty i limity Image Optimization w Next.js

next/image optymalizuje obrazy na żądanie, co jest wygodne, ale ma 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. Potencjalną liczbę wpisów w cache wyznaczają źródła, żądane szerokości, formaty oraz jakości, ale warianty powstają dopiero po konkretnych żądaniach. Sprawdź realny ruch i rachunki hostingu zamiast zakładać, że każdy teoretyczny wariant zostanie wygenerowany.

AVIF zwykle zmniejsza transfer względem WebP, ale pierwsze kodowanie trwa dłużej, a każdy format zajmuje osobny cache. Next.js nadal rekomenduje WebP dla większości zastosowań. Jeśli włączasz kilka formatów i stawiasz proxy lub CDN przed własnym serwerem Next.js, warstwa pośrednia musi przekazywać nagłówek Accept.

Bezpieczne źródła obrazów w Next.js

Nie używaj hostname: '**' ani nie pomijaj wszystkich pozostałych ograniczeń. Taka konfiguracja pozwala osobom trzecim wykorzystywać Twój Image Optimization API do pobierania i przetwarzania niezamierzonych URL-i. Dla lokalnych źródeł możesz analogicznie zastosować localPatterns.

Natywny <img> z srcset: kiedy wystarczy zamiast next/image?

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"
    srcset="
      /images/hero-400.jpg   400w,
      /images/hero-800.jpg   800w,
      /images/hero-1200.jpg 1200w
    "
    sizes="(max-width: 768px) 100vw, 50vw"
    alt="Panel analityczny z wykresem czasu ładowania obrazów"
    width="1200"
    height="630"
    decoding="async"
    fetchpriority="high"
  />
</picture>

Kiedy wybrać natywny srcset

  • Static export (output: 'export'): wbudowany optimizer wymaga serwera; w eksporcie użyj zewnętrznego loadera, unoptimized albo własnego <picture>.
  • 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 wybór rozmiaru tego samego obrazu, a <picture> pozwala wybrać format albo inny kadr. W przykładzie hero nie ma loading="lazy", ponieważ opóźniałoby to kandydata LCP. JPEG również ma responsywny srcset, więc fallback nie oznacza automatycznie pobrania jednego dużego pliku.

Code
import { getImageProps } from 'next/image'
 
export function ResponsiveHero() {
  const common = {
    alt: 'Panel analityczny kampanii Google Ads na ekranie laptopa',
    sizes: '100vw',
    loading: 'eager' as const,
    fetchPriority: 'high' as const,
  }
 
  const {
    props: { srcSet: desktop },
  } = getImageProps({
    ...common,
    src: '/images/hero-desktop.jpg',
    width: 1600,
    height: 900,
    quality: 75,
  })
 
  const {
    props: { srcSet: mobile, ...fallback },
  } = getImageProps({
    ...common,
    src: '/images/hero-mobile.jpg',
    width: 800,
    height: 1000,
    quality: 75,
  })
 
  return (
    <picture>
      <source media="(min-width: 768px)" srcSet={desktop} />
      <source media="(max-width: 767px)" srcSet={mobile} />
      <img {...fallback} style={{ width: '100%', height: 'auto' }} />
    </picture>
  )
}

Tego nie zastąpisz samym sizes, ponieważ sizes nie zmienia kadru. getImageProps() zachowuje optymalizację i generowanie srcset przez Next.js, a <picture> odpowiada za art direction. Nie dodawaj tutaj preload, jeśli różne viewporty mają różnych kandydatów LCP; loading="eager" i fetchPriority="high" trafią do elementu <img> przez fallback.

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

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

Integracja CDN z next/image przez custom loader

Code
// lib/cloudinary-loader.ts
'use client'
 
import type { ImageLoaderProps } from 'next/image'
 
export default function cloudinaryLoader({
  src,
  width,
  quality,
}: ImageLoaderProps) {
  const params = [`w_${width}`, `q_${quality ?? 75}`, 'f_auto', 'c_limit']
 
  const publicId = src.replace(/^\/+/, '')
 
  return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}/${publicId}`
}
Code
// next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  images: {
    loader: 'custom',
    loaderFile: './lib/cloudinary-loader.ts',
  },
}
 
export default nextConfig

Po tej konfiguracji każde użycie next/image korzysta z Cloudinary, a sizes nadal steruje wygenerowanym srcset. Globalny loader ma sens tylko wtedy, gdy wszystkie przekazywane źródła należą do tego samego modelu URL. W aplikacji mieszającej lokalne importy i Cloudinary zastosuj loader tylko w dedykowanym komponencie albo rozdziel sposób renderowania obu grup.

CDN jest dobrym rozwiązaniem, jeśli źródło obrazów jest zmienne lub już należy do CMS-a oferującego transformacje. Skala biblioteki jest ważna, ale nie stanowi jedynego kryterium: nawet mały serwis na Sanity może skorzystać z istniejącego image CDN, a duży katalog z kilkoma stałymi wariantami może działać bez osobnej usługi.

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.

next/image vs srcset vs CDN: porównanie podejść

Kryteriumnext/image z domyślnym loaderemNatywny srcsetImage CDN z loaderem
Konwersja formatuwedług images.formatsw pipeline buildawedług możliwości dostawcy
Responsywne szerokościgenerowany srcsetwarianty przygotowane ręcznietransformacje URL i generowany srcset
Lazy loadingdomyślnie włączoneprzez loading="lazy"zależy od użytego elementu lub komponentu
Rezerwacja miejscawymagane wymiary albo fillręczne wymiary lub CSSzależy od integracji w HTML
Blur placeholderautomatyczny dla obsługiwanych importówwłasny LQIPzależy od dostawcy
KosztCPU, cache lub opłata hostingubuild, storage i transferusługa, transformacje i transfer
Static exportloader albo unoptimizeddziaładziała przez URL-e lub loader
Kontrolawysoka w ramach API Next.jspełnawysoka w ramach API dostawcy

Jak wybrać optymalizację obrazów w Next.js?

Najprostszy sposób podjęcia 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. CMS już oferuje transformacje albo potrzebujesz ich na żądanie? Użyj jego image CDN lub loadera.
  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 optymalizacji obrazów

Obraz LCP: preload, loading="eager" albo fetchPriority="high"

W Next.js 16 prop preload zastąpił przestarzały priority. Użyj go, gdy obraz jest jednoznacznym kandydatem na i powinien zostać odkryty już w <head>. Nie oznaczaj w ten sposób całego rzędu kart, ponieważ preload wymusza pobranie zasobu i może konkurować z CSS, fontami oraz właściwym LCP.

Nie łącz preload z loading ani fetchPriority. Gdy kandydat zależy od viewportu albo obraz jest wcześnie widoczny i łatwo odkrywalny w HTML, częściej wystarczy loading="eager" lub fetchPriority="high". Preload odpowiada za wczesne odkrycie, a Fetch Priority jest wskazówką dotyczącą ważności pobierania; to nie są synonimy.

Poprawny alt dla SEO i dostępności

Code
// Zbyt ogólny tekst alternatywny.
<Image src={product.image} alt="zdjęcie" />
 
// Obraz informacyjny: alt zastępuje informację przekazywaną przez obraz.
<Image src={product.image} alt="Buty do biegania Nike Air Max 90 w kolorze białym" />
 
// Obraz czysto dekoracyjny: pusty alt ukrywa go przed czytnikiem ekranu.
<Image src="/decorative-line.png" alt="" width={800} height={20} />

Pierwszy tekst jest zbyt ogólny, drugi może być poprawny, jeśli kolor i model są istotną informacją niewystępującą obok. Nie duplikuj w alt podpisu, nazwy produktu ani tekstu linku tylko po to, żeby dodać słowa kluczowe.

Unikaj layout shift przez aspect ratio

Przy width i height przeglądarka zna proporcje przed pobraniem pliku. Przy fill rodzic musi mieć stabilny rozmiar oraz pozycjonowanie, na przykład klasy relative aspect-video pokazane wcześniej. Samo position: relative bez wysokości albo aspect-ratio nie rezerwuje miejsca.

Mierz realny wpływ obrazów na wydajność

Po zmianie obrazów sprawdź:

  1. Czy LCP wskazuje właściwy element w PageSpeed Insights lub WebPageTest?
  2. Czy zasób LCP został odkryty wcześnie i nie ma loading="lazy"?
  3. Czy pobrany rozmiar obrazu odpowiada layoutowi, a nie pełnej szerokości ekranu?
  4. Czy serwer zwrócił oczekiwany Content-Type, Cache-Control i rozmiar transferu?
  5. 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, Priority i Content-Type, a potem porównaj mobile i desktop z wyłączonym cache. W konsoli sprawdź img.currentSrc, img.naturalWidth i img.clientWidth. Jeśli karta o szerokości 280 px regularnie pobiera obraz 1200 px, sprawdź sizes, gęstość ekranu i dostępne szerokości w srcset.

Panel Performance pokaże, czy problemem jest późne odkrycie URL-a, długie pobieranie, czy renderowanie po zakończeniu requestu. Test laboratoryjny powtórz na ograniczonym CPU i sieci, a decyzje produkcyjne potwierdź danymi terenowymi z Core Web Vitals.

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ł.

Czy AVIF jest lepszy od WebP?

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.

Jak działa next/image w static export?

Wbudowane Image Optimization API nie działa w static export, ponieważ optymalizuje obrazy podczas żądania. Możesz użyć zewnętrznego loadera, ustawić unoptimized albo samodzielnie przygotować picture i srcset.

Którym propem oznaczyć obraz LCP w Next.js 16?

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".

Po co podawać prop sizes, skoro mam width i height?

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.

Co zmienia images.qualities w Next.js 16?

qualities to allowlista dozwolonych wartości quality. Next.js 16 używa domyślnie [75], więc własna konfiguracja jest potrzebna dopiero wtedy, gdy korzystasz z innych jakości. Dla wartości spoza listy komponent wybierze najbliższą, a bezpośrednie żądanie API otrzyma odpowiedź 400.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web 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.

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

Skontaktuj się ze mną
LH.pl – Hosting Mango

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Kiedy Next.js daje przewagę w SEO nad React SPA

Next.js to framework React, który ułatwia uniknięcie głównego ograniczenia aplikacji SPA renderowanych wyłącznie w przeglądarce: kluczowa treść może znaleźć się już w początkowym HTML. Google nie musi wtedy czekać na wykonanie JavaScriptu, żeby ją odkryć. Framework dodaje też Metadata API, optymalizację obrazów i podział kodu, ale korzyść dla Core Web Vitals zależy od sposobu implementacji, ilości kodu klienckiego i infrastruktury.

Maciej Sala

Maciej Sala

Founder StriveLab

Jak poprawić Core Web Vitals i czy to pomaga w Google?

Core Web Vitals pokazują, jak szybko strona wyświetla główną treść, reaguje na działania użytkownika i czy jej układ pozostaje cały czas stabilny. Mimo że Google bierze pod uwagę te metryki przy ustalaniu pozycji, sam „zielony raport” nie zapewnia sukcesu w wynikach wyszukiwania. Najważniejszym czynnikiem zawsze pozostaje merytoryczna i dopasowana do intencji użytkownika treść . W związku z powyższym, by optymalizacja miała sens, trzeba rozpoznać problem w danych, dobrać poprawkę i sprawdzić jej efekty u użytkowników.

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

LH.pl – Cloud Server 1C4G