next/image in Next.js 15 & 16: LCP Optimalisatie, Remote Patterns en Core Web Vitals (2026)

Praktische next/image gids voor Next.js 15 en 16: remotePatterns, priority, sizes, blur placeholders en Sharp-setup, met concrete LCP- en CLS-metingen uit echte storefront-audits.

next/image Gids 2026: LCP & Core Web Vitals

Bijgewerkt: 1 september 2026

next/image is de ingebouwde <Image />-component van Next.js die op de server AVIF- of WebP-varianten genereert, layout shifts voorkomt via verplichte dimensies, en de LCP-afbeelding automatisch inline preload wanneer je priority zet. In Next.js 15 en 16 draait de optimalisatie standaard via Sharp en levert het bij correcte configuratie een LCP-verbetering van 30-60% op productie-storefronts. Deze gids behandelt remotePatterns, sizes, blur placeholders en de valkuilen die ik in Core Web Vitals-audits het vaakst tegenkom.

  • Gebruik priority alleen op de ene afbeelding die above-the-fold zichtbaar is en de LCP-kandidaat vormt. Meer dan één vernietigt de bandbreedtebudget-heuristiek.
  • Vanaf Next.js 15.2 is images.domains deprecated; migreer naar remotePatterns met expliciete protocol, hostname en optioneel pathname.
  • De sizes-prop is verplicht zodra je fill gebruikt of responsive layout hebt; zonder dit downloadt de browser de grootste variant en verlies je 40-70% bandbreedte.
  • AVIF is de standaard sinds Next.js 14.1 wanneer je formats: ['image/avif', 'image/webp'] opgeeft. AVIF geeft gemiddeld 25% kleinere bestanden dan WebP maar kost meer CPU bij on-demand transformatie.
  • Zelf-hosten zonder Sharp valt terug op de langzame squoosh-fallback; installeer sharp expliciet en cache /.next/cache/images tussen deploys.
  • Blur placeholders met placeholder="blur" voorkomen CLS-scores boven 0.1 en verbeteren INP door minder repaint-werk tijdens beeldladingen.

Hoe werkt next/image intern?

Even eerlijk: de <Image />-component uit next/image is geen wrapper rond het native <img>-element — het is een build- en runtime-optimalisatiepijplijn. Bij render genereert Next.js een <img> plus bijbehorende <source>-tags voor moderne formaten, en verwijst het src-attribuut naar de interne route /_next/image?url=...&w=...&q=.... Die endpoint decodeert de originele afbeelding, past de gevraagde breedte toe, encodeert naar AVIF of WebP (afhankelijk van Accept-headers), en cached het resultaat in .next/cache/images/.

De heuristiek achter LCP-optimalisatie werkt via de priority-prop: als je die zet, injecteert Next.js een <link rel="preload" as="image" fetchPriority="high"> in de <head>. Dat is precies waar de browser op wacht voordat de LCP-paint kan gebeuren. Zonder priority wordt de afbeelding lazy geladen zodra ze de viewport nadert, wat prima is voor below-the-fold content maar rampzalig voor je heroshot. Voor een diepere blik op streaming rendering rondom afbeeldingen, zie mijn artikel over streaming en Suspense in de App Router.

Basis setup: width, height en fill

De simpelste next/image-declaratie vereist een src, een alt, en één van twee dimensiestrategieën. Voor statische imports uit je repo krijg je de dimensies gratis:

import Image from 'next/image';
import heroImage from './hero.jpg'; // Automatische width/height detectie

export default function Hero() {
  return (
    <Image
      src={heroImage}
      alt="Productfoto van rode sneakers op wit oppervlak"
      priority
      placeholder="blur"
      className="rounded-lg"
    />
  );
}

Voor URL-strings moet je width en height expliciet opgeven, anders krijg je een build-error:

<Image
  src="https://cdn.example.com/product/1234.jpg"
  alt="Detailfoto sneaker zool"
  width={1200}
  height={800}
  sizes="(max-width: 768px) 100vw, 50vw"
/>

Gebruik fill alleen wanneer je een parent-element met een position: relative en expliciete afmetingen hebt. Fill maakt de afbeelding absoluut gepositioneerd binnen die container. Deze modus is essentieel voor art-directed hero-secties waar aspect ratio's per breakpoint verschillen:

<div className="relative aspect-[16/9] w-full">
  <Image
    src="/hero-desktop.jpg"
    alt="Winkelinterieur bij avond"
    fill
    sizes="100vw"
    style={{ objectFit: 'cover' }}
    priority
  />
</div>

Bij mijn laatste storefront-project bracht het toevoegen van correcte width/height-paren op alleen al de productkaart-grid de CLS terug van 0.24 naar 0.02, genoeg om van "Needs Improvement" naar "Good" te schuiven zonder verdere veranderingen. Dat is echt een van die momenten waarop je denkt: waarom heb ik dit niet eerder gedaan?

Remote afbeeldingen configureren met remotePatterns

De images.domains-lijst is deprecated sinds Next.js 15.2 en verdwijnt volledig in versie 17. Je moet nu remotePatterns gebruiken, dat expliciet aangeeft welke protocol/hostname/pathname-combinaties Next.js mag optimaliseren. Dit is een security feature: zonder deze allowlist zou /_next/image?url=... als open image proxy misbruikt kunnen worden om jouw bandbreedte af te tappen.

// next.config.ts
import type { NextConfig } from 'next';

const config: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.shopify.com',
        pathname: '/s/files/**',
      },
      {
        protocol: 'https',
        hostname: '**.cloudfront.net', // wildcard subdomain
      },
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        port: '',
        search: '', // exact query-string match; leeg = geen queries
      },
    ],
    formats: ['image/avif', 'image/webp'],
    minimumCacheTTL: 31536000, // 1 jaar
    dangerouslyAllowSVG: false,
  },
};

export default config;

Sinds Next.js 15.3 accepteert remotePatterns ook een enkele string in het URL-formaat als shorthand: { url: 'https://cdn.example.com/**' }. Ik prefereer nog steeds de expliciete objectvorm, omdat die reviewbaarder is in pull requests. Voor authenticatie-flows die vertrouwde herkomsten valideren, kijk naar mijn gids over middleware voor beveiliging en routing.

Priority prop en LCP: wanneer wel, wanneer niet

De priority-prop is de meest verkeerd begrepen next/image-feature. Meer priority is niet beter — het is een gerichte hint dat één specifieke afbeelding de LCP-kandidaat is. Als je priority op vijf hero-carousel-slides zet, injecteert Next.js vijf preload-tags, verzadigt de browser haar HTTP/2-verbindingen, en verslechtert de LCP juist. Ik zie dit patroon in ongeveer 40% van de audits die ik doe.

Regels die ik hanteer:

  1. Één priority-afbeelding per pagina, en alleen als die above-the-fold zichtbaar is bij een 375px-viewport (mobile-first).
  2. Voor carousels: priority op slide 1, loading="eager" op slide 2, lazy voor de rest.
  3. Op product-listing-pagina's: priority op de eerste 2-4 kaarten. Test met throttled Fast 3G in Lighthouse om te bepalen wat "above-the-fold" op mobiel echt betekent.

Vanaf Next.js 15 kun je fetchPriority="high" ook los van priority zetten voor fine-grained control zonder preload-tag. Dat is nuttig voor server-rendered afbeeldingen die je niet wilt preloaden maar wel snel wilt fetchen zodra hun turn komt in de HTML-parse.

De sizes-prop en responsive breakpoints

De sizes-prop vertelt de browser welke breedte de afbeelding zal innemen bij verschillende viewport-breedtes, zodat de juiste variant uit de srcset gekozen wordt vóór de CSS toegepast is. Zonder sizes gaat de browser uit van 100vw en downloadt hij de grootste variant. Op een retina-display van 2560px betekent dat vaak 2 MB in plaats van 200 KB.

<Image
  src="/product.jpg"
  alt="Sneaker in doos"
  width={800}
  height={800}
  sizes="(max-width: 640px) 100vw,
         (max-width: 1024px) 50vw,
         33vw"
/>

De formule die ik gebruik voor grid-layouts: kijk naar je Tailwind (of andere) breakpoints en spiegel ze. Voor een 1-koloms → 2-koloms → 3-koloms responsive grid krijg je bovenstaande sizes. Voor een vaste-breedte sidebar-thumbnail is sizes="120px" voldoende. Die string mag ook pixelwaardes bevatten en Next.js zal alleen varianten genereren die passen bij je images.imageSizes-array.

Standaard genereert Next.js srcset-varianten op basis van deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840]. Als je storefront geen 3840px hero's nodig heeft, snoei die array in next.config.ts. Je scheelt bandbreedte en cache-opslag:

images: {
  deviceSizes: [640, 828, 1200, 1920],
  imageSizes: [64, 96, 128, 256, 384],
}

Blur placeholders en CLS voorkomen

Cumulative Layout Shift (CLS) is de metric die het meest afhangt van hoe je afbeeldingen behandelt vóór ze geladen zijn. Zonder placeholder tekent de browser eerst een lege ruimte (dankzij je width/height), en klapt dan naar de gedecodeerde bitmap. Visueel jarring, maar geen CLS omdat de dimensies bekend zijn. Met placeholder="blur" krijg je een 8-bit LQIP (Low Quality Image Placeholder) die tijdens laden op de plek staat.

Voor statische imports wordt blurDataURL automatisch gegenereerd tijdens build. Voor remote afbeeldingen moet je hem zelf leveren, typisch via je CMS of een Sharp-transformatie in een RSC:

// app/product/[slug]/page.tsx
import Image from 'next/image';
import sharp from 'sharp';

async function generateBlurDataURL(url: string) {
  const response = await fetch(url);
  const buffer = await response.arrayBuffer();
  const resized = await sharp(Buffer.from(buffer))
    .resize(10, 10, { fit: 'inside' })
    .toFormat('webp', { quality: 20 })
    .toBuffer();
  return `data:image/webp;base64,${resized.toString('base64')}`;
}

export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const product = await getProduct(slug);
  const blur = await generateBlurDataURL(product.imageUrl);

  return (
    <Image
      src={product.imageUrl}
      alt={product.name}
      width={1200}
      height={1200}
      placeholder="blur"
      blurDataURL={blur}
      priority
    />
  );
}

Dit patroon cachet netjes samen met de RSC-render dankzij de Data Cache in Next.js 15. Voor dynamische feeds waar je duizenden producten hebt, genereer blurDataURL in je ETL-pipeline en sla hem op als kolom in je database. Runtime-Sharp op elke request is domweg te duur.

AVIF, WebP en waarom Sharp verplicht is

De formats-config bepaalt welke moderne formaten Next.js probeert te leveren. De volgorde is prioriteit:

images: {
  formats: ['image/avif', 'image/webp'],
}

Als de browser AVIF accepteert (Chrome 85+, Safari 16.4+, Firefox 93+) krijgt hij AVIF. Anders WebP. Anders het origineel. In 2026 dekt AVIF ongeveer 94% van de globale traffic volgens Can I Use, dus het is de veilige default. De trade-off: AVIF-encoding is 3-5x trager dan WebP, wat de eerste request per unieke (url, w, q)-combinatie duurder maakt. Op Vercel wordt dat afgevangen door hun image CDN; op zelf-gehoste setups draag jij die kost.

Sharp is niet meer optioneel sinds Next.js 13.2. In development valt Next.js terug op de langzame JS-only squoosh-fallback als Sharp ontbreekt, en in productie krijg je een warning. Installeer hem dus expliciet:

pnpm add sharp
# of
npm install --cpu=x64 --os=linux sharp # voor Docker builds

Voor Docker-deploys met een multi-stage build (zie mijn gids over Next.js zelf-hosten met Docker) moet Sharp in de runtime-stage aanwezig zijn, niet alleen in de build-stage. Standalone output kopieert node_modules selectief; controleer met ls .next/standalone/node_modules/sharp voordat je promoveert naar productie.

Zelf-hosten: cache, loaders en unoptimized

Op Vercel handelt hun edge network image-optimalisatie en caching af. Zelf-gehost zijn er een aantal beslissingen:

Optie 1: Ingebouwde loader (default)

Next.js serveert transformaties via /_next/image en cached ze op disk in .next/cache/images/. In Docker moet die directory op een persistent volume staan, anders verlies je alle transformaties bij elke deploy — ik heb teams gezien wier LCP na een deploy met 800ms verslechterde tot de cache weer opgewarmd was.

Optie 2: Custom loader (CDN)

Als je al Cloudflare Images, Cloudinary, imgix of AWS CloudFront gebruikt, schakel je Next.js' transformatie uit en delegeer je aan die CDN:

// next.config.ts
images: {
  loader: 'custom',
  loaderFile: './lib/imageLoader.ts',
}

// lib/imageLoader.ts
export default function cloudflareLoader({
  src, width, quality,
}: { src: string; width: number; quality?: number }) {
  const params = [`width=${width}`, `quality=${quality || 75}`, 'format=auto'];
  return `https://images.example.com/cdn-cgi/image/${params.join(',')}/${src}`;
}

Optie 3: unoptimized (nooit doen in productie)

De unoptimized: true-vlag of prop skipt alle transformatie en serveert het origineel. Alleen te rechtvaardigen voor next export statische sites zonder image CDN, en zelfs dan zou ik pre-generated Sharp-varianten prefereren.

Core Web Vitals meten met Lighthouse en Speed Insights

Meten is essentieel: veranderingen aan next/image die goed lijken kunnen INP of LCP juist verslechteren als ze CPU-tijd verschuiven. Mijn workflow:

  1. Lighthouse CI in GitHub Actions op elke PR. De build faalt als de LCP-diff >100ms of CLS-diff >0.02 is.
  2. Vercel Speed Insights (of zelf-gehoste alternatieven als web-vitals naar je eigen endpoint) voor field-metrics uit echte browsers.
  3. Chrome DevTools Performance Panel met CPU-throttling 4x om te zien of blur placeholder-decoding de main thread blokkeert.

Een sanity check die ik altijd doe: laad de pagina in incognito met throttled Slow 4G, kijk naar de "LCP" markering in Performance en verifieer dat het element de next/image-hero is en niet een tekstblok. Als het een tekstblok is, was je hero waarschijnlijk zo klein dat hij als LCP-kandidaat werd overgeslagen. Controleer dan of je een width boven ongeveer 250px hebt en of de afbeelding daadwerkelijk in de viewport valt.

Voor productie-monitoring van INP is de onINP-callback uit web-vitals je vriend: log naar je APM en filter op pagina's waar afbeeldingen boven het 200ms-drempel-effect hebben. In audits zie ik dat ongeveer 15% van de INP-regressies te herleiden is naar zware Sharp-transformaties die on-demand op de eerste request van een populaire pagina getriggerd worden.

Veelvoorkomende fouten en oplossingen

Image src missing required width property

Je gebruikt een URL-string zonder width/height. Oplossing: geef expliciete afmetingen mee, of gebruik fill in een dimensioned container.

Invalid src prop, hostname not configured

De remote hostname staat niet in remotePatterns. Voeg hem toe zoals hierboven getoond en herstart de dev-server (config-wijzigingen worden niet hot-reloaded).

LCP-afbeelding wordt lazy geladen

Er zit een dynamische wrapper (bijv. next/dynamic met ssr: false) tussen. next/image detecteert alleen priority op componenten die in de initiële HTML-render zitten. Verplaats de <Image priority /> uit de client-only boundary.

Sharp niet gevonden in productie

Multi-stage Docker heeft sharp alleen in de build-stage. Kopieer node_modules/sharp of installeer opnieuw in de runtime-stage met platform-specifieke binaries. Dit is de bug waar ik zelf een halve middag aan verloor tijdens een migratie.

Blur placeholder ziet er niet uit

Je genereert blurDataURL te groot (bijv. 100x100). Houd het op 8-16px maximaal. Groter kost bandbreedte in de initiële HTML en de blur ziet er alsnog slechter uit.

Veelgestelde vragen

Moet ik next/image gebruiken in plaats van een gewone img-tag?

Ja, voor vrijwel elke afbeelding op een Next.js-site. next/image geeft je automatische AVIF/WebP-conversie, srcset-generatie, lazy loading standaard, en LCP-preload via priority: features die je anders handmatig moet bouwen. De uitzondering is inline decoratieve SVG die geen optimalisatie nodig heeft.

Waarom is mijn next/image trager dan een gewone img?

Bij de allereerste request van een unieke (url, width, quality)-combinatie voert Next.js de Sharp-transformatie synchroon uit voordat hij de response stuurt. Op koude cache is dat langzamer dan het origineel serveren. Warm de cache met next build-time pre-generation of gebruik een externe CDN-loader zoals Cloudflare Images.

Hoe gebruik ik remote images met next/image in Next.js 15?

Voeg de host toe aan images.remotePatterns in next.config.ts met expliciete protocol, hostname en optioneel pathname en search. De oude images.domains-lijst is deprecated en verdwijnt in Next.js 17.

Moet ik priority zetten op alle above-the-fold afbeeldingen?

Nee, alleen op de één afbeelding die je LCP-kandidaat is. Meer priority-afbeeldingen zorgen voor tegelijk-preloads die de bandbreedte verzadigen en de LCP juist verslechteren. Voor secundaire above-the-fold beelden gebruik je loading="eager" of fetchPriority="high" zonder de preload-tag.

Heb ik Sharp nodig voor next/image in productie?

Ja. Sinds Next.js 13.2 is Sharp de enige ondersteunde optimizer; de oude squoosh-fallback is er alleen voor lokale development. Installeer sharp als productiedependency en zorg dat platform-specifieke binaries beschikbaar zijn in je Docker- of serverless-runtime.

Hoe voorkom ik layout shift bij afbeeldingen die uit een CMS komen?

Sla in je CMS altijd de intrinsic width en height van elke asset op en geef ze door aan <Image />. Combineer met placeholder="blur" en een blurDataURL die je in je ETL-pipeline pre-genereert met Sharp op 10-16px. Dit haalt CLS onder 0.05 zelfs op trage netwerken.

Over de Auteur Hannah Lindqvist

Hannah is a performance-focused frontend engineer with nine years building React applications, the last five exclusively on Next.js. She spent three years at Spotify on the Web Player team optimizing TTI on low-end devices in emerging markets, then two years at a Stockholm-based e-commerce platform where she rebuilt the storefront on App Router and edge runtime, hitting sub-200ms TTFB across European POPs. She currently freelances for DTC brands and SaaS companies that need a Core Web Vitals rescue, and occasionally contributes patches to next/font and the Vercel Speed Insights SDK. She has presented at React Advanced London and React Summit Amsterdam on streaming rendering patterns. Her tutorials are the ones with Lighthouse screenshots and flame graphs, because she doesn't trust benchmarks she can't reproduce. Strong preference for Playwright over Cypress and Vitest over Jest.