Next.js Image Optimisation: The Complete Guide
Master Next.js image optimisation with the Image component, lazy loading, responsive sizing, and format conversion to dramatically improve page speed and Core Web Vitals.
Images are almost always the heaviest assets on any website — and on hospitality, restaurant, and service business sites, that problem is magnified. Hero shots, gallery pages, team photos, dish photography: they're essential for conversions, but handled badly they'll tank your Core Web Vitals, inflate your bounce rate, and quietly cost you organic rankings.
The good news is that Next.js ships with one of the most powerful image optimisation pipelines available in any web framework. When you use it properly, it handles WebP/AVIF conversion, responsive sizing, lazy loading, and layout stability automatically. The bad news is that most sites we audit are barely scratching the surface of what it offers.
This guide covers everything: from the basics of the <Image> component to advanced configuration for production use. By the end, you'll have a clear picture of what to implement and why it matters.
Why images destroy performance (and why Next.js solves it)
Before diving into the API, it helps to understand exactly what makes images so damaging to performance when handled naively.
File size is the obvious one. A 4MB JPEG hero image will cripple a mobile user on a 4G connection before they've even seen your above-the-fold content. But switching to WebP typically reduces that to under 800KB. AVIF, the newer format, can cut it to 400KB with no perceptible quality loss. The problem is that older browsers don't support AVIF, so you need to serve the right format per browser — which is fiddly to implement manually.
Dimensions matter just as much. If you're displaying a 600px-wide thumbnail but serving a 2400px original, the browser is downloading four times more data than it needs. Responsive images (using srcset) solve this, but writing srcset manually for every image is tedious and error-prone.
Layout shift (measured as Cumulative Layout Shift in Core Web Vitals) happens when the browser doesn't know an image's dimensions before it loads, so surrounding content jumps around as images appear. Google counts this against your search ranking and it's infuriating for users.
Lazy loading means only loading images when they're near the viewport, rather than loading every image on page load. This dramatically reduces initial page weight but requires careful implementation to avoid delaying above-the-fold images.
Next.js's <Image> component handles all four of these automatically. That's the pitch. Let's look at how it works.
The <Image> component: the essentials
The <Image> component is a drop-in replacement for the standard HTML <img> tag, imported from next/image:
```tsx import Image from 'next/image';
export default function HeroSection() {
return (
A few things are happening here that don't happen with a plain <img>:
- Next.js automatically generates WebP and AVIF versions of the image and serves the best format the browser supports
- It generates multiple sizes and serves the appropriate one based on the device's viewport
- It reserves the space in the layout before the image loads, preventing layout shift
- The
priorityprop tells Next.js to preload this image (more on that below)
The priority prop
Use priority on any image that's visible above the fold — typically your hero image and any above-the-fold content images. Without it, Next.js lazy loads the image, which is correct for most images but will hurt your Largest Contentful Paint (LCP) score if the main hero is lazy loaded.
A common mistake we see: developers forget priority on hero images and then wonder why their LCP score is poor despite fast server response times. The LCP element is often the hero image — if it's lazy loaded, you've introduced unnecessary delay.
fill mode for flexible containers
When you don't know the exact dimensions in advance — say, a CMS-sourced image filling a card of varying height — use the fill prop instead of explicit width and height:
``tsx
<div style={{ position: 'relative', height: '400px' }}>
<Image
src={post.coverImage}
alt={post.title}
fill
style={{ objectFit: 'cover' }}
/>
</div>
``
The parent container must have position: relative (or absolute / fixed). The objectFit: 'cover' style ensures the image fills the container without distortion, cropping where needed — essential for consistent card layouts.
sizes for accurate responsive behaviour
The sizes prop tells the browser what size the image will actually be rendered at different viewport widths. Without it, Next.js makes a conservative assumption and may serve a larger image than necessary.
``tsx
<Image
src="/images/dish.jpg"
alt="Seared sea bass with celeriac purée"
width={600}
height={400}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
``
This tells the browser: on mobile, this image fills the full width; on tablets, it's half the width; on desktop, it's a third. The browser then downloads only the size it needs. Without sizes, the browser defaults to 100vw, potentially downloading an unnecessarily large image on desktop if the image is actually narrow.
Getting sizes right is one of the highest-impact, least-implemented optimisations we encounter in audits. It's worth spending five minutes setting it correctly on every significant image.
Remote images and configuration
If your images are hosted externally — on a CDN, Cloudinary, Supabase Storage, or a CMS — you need to declare the allowed domains in next.config.js:
``js
// next.config.js
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'res.cloudinary.com',
},
{
protocol: 'https',
hostname: '*.supabase.co',
},
],
},
};
``
Use remotePatterns rather than the older domains property — it's more flexible and supports wildcards in subdomains. The domains field is still supported but is considered legacy.
Using a custom image loader
For high-volume sites or those already using a CDN like Cloudinary or Imgix, you can replace Next.js's built-in image optimiser with a custom loader. This offloads transformation to the CDN, which can be faster and cheaper at scale:
``tsx
const cloudinaryLoader = ({ src, width, quality }) => {
return https://res.cloudinary.com/your-account/image/upload/w_${width},q_${quality || 75}/${src}`;
};
For most sites under moderate traffic, the built-in optimiser is fine. For sites receiving tens of thousands of image requests per day, a CDN loader is worth the additional setup.
Format configuration and quality tuning
By default, Next.js serves WebP to browsers that support it and JPEG to those that don't. You can extend this to include AVIF:
``js
// next.config.js
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
},
};
``
AVIF offers significantly better compression than WebP — often 30-50% smaller at the same quality. The trade-off is that AVIF encoding is CPU-intensive, so it takes longer to generate on the server at first request. After that it's cached, so subsequent requests are fast. For most production sites, enabling AVIF is worthwhile.
You can also set a default quality level (0-100, defaults to 75):
``js
images: {
quality: 80,
}
``
Or per image with the quality prop: <Image ... quality={90} />. Photography-heavy sites like restaurant galleries often benefit from 80-85, which keeps images visually sharp while reducing file size.
Common mistakes we fix in audits
Using plain <img> tags
The most frequent issue: developers use <img> out of habit, especially when migrating from older frameworks. You lose all optimisation, lazy loading defaults, and layout stability guarantees. Replace every <img> with <Image> from next/image. The only legitimate exception is images that are already tiny (icons, SVGs) or dynamically injected into markdown content — and even then there are patterns to handle those correctly.
Forgetting alt text
This one affects both accessibility and SEO. Every <Image> must have meaningful alt text that describes what's in the image. For decorative images that add no information, use alt="" rather than omitting the prop entirely — the component requires it, but an empty string correctly signals to screen readers that the image is decorative.
Importing images without dimensions
When importing local images (from your public folder or src/assets), Next.js can infer the dimensions automatically:
```tsx import heroImage from '@/assets/hero.jpg';
This is the safest approach for local images — no need to specify width and height manually, and you won't accidentally get them wrong.
Blurring placeholder on external images
Next.js supports a low-quality placeholder blur (like Medium's lazy loading effect) using placeholder="blur". For local images, it generates the blur data URI automatically. For external images, you need to provide blurDataURL yourself — a tiny base64-encoded thumbnail of the image. Forgetting this on external images causes errors.
Real-world impact: what the numbers look like
We recently audited a hotel client's site before taking over development. Their homepage was loading 4.2MB of images. After switching to the Next.js <Image> component with proper sizes attributes and AVIF enabled, the same visual output was being delivered in 840KB — an 80% reduction. Their LCP score went from 5.8s to 1.4s. Organic traffic increased 34% over the following three months, largely attributable to the improved Core Web Vitals scores.
These numbers are typical for image-heavy hospitality sites. The images themselves haven't changed — same photography, same design — but the delivery mechanism has.
Key takeaways
- **Replace every
<img>with<Image>** — it's the single highest-ROI change you can make on a Next.js site - **Add
priority** to every above-the-fold image, especially hero images - **Set
sizesaccurately** — it prevents oversized images being downloaded on narrow viewports - Enable AVIF in
next.config.jsfor significant file size reductions at no visual cost - **Use
fill+objectFit: 'cover'** for flexible containers like cards and galleries - **Use
remotePatterns** for external image sources — thedomainsfield is legacy - Check your LCP element in Chrome DevTools' Performance tab — it's almost always an image, and almost always improvable
Want images that don't slow your site down?
Image optimisation is one of those things that looks simple until you're deep in responsive breakpoints, CDN configuration, and AVIF encoding times. Done well, it's invisible — your site loads fast, your visitors see beautiful photography, and Google rewards you for it. Done badly, it's one of the most common reasons otherwise well-built sites underperform in search.
We handle this as standard in all our builds. If you're working with a hospitality or service business site that's heavy on photography and slow to load, we'd be happy to take a look and give you an honest view of what's holding it back. Get in touch to start the conversation.
Need help implementing this?
We build high-performance websites and automate workflows for ambitious brands. Let's talk about how we can help your business grow.
More Articles

Next.js App Router: A Practical Migration Guide
Still on the Pages Router? Here's how to migrate to Next.js App Router incrementally — without breaking production or rewriting everything at once.

Core Web Vitals for Hospitality Websites (Fix These First)
Slow load times and layout shifts lose hotel and restaurant bookings before visitors even see your rooms. Here's how to diagnose and fix Core Web Vitals on hospitality sites.