Tech Verse Logo
Enable dark mode
Next.js Image Optimization: Sizes and Remote Patterns

Next.js Image Optimization: Sizes and Remote Patterns

Md. Mostafijur RahmanMMd. Mostafijur Rahman

Md. Mostafijur Rahman

5 min read

Why Next.js Image Optimization Breaks in Production

You install Next.js 16, wrap your product thumbnails in the <Image /> component, and assume your web vitals will instantly turn green. Then your Lighthouse report drops to 62, Cumulative Layout Shift (CLS) spikes to 0.28, and your build throws 400 Bad Request errors for external avatar URLs. Next.js does a lot of heavy lifting under the hood, but it cannot guess your layout context or parse permissive remote domain wildcards safely.

The standard next/image component wraps HTML <img> elements with custom loader logic, automatic WebP/AVIF generation, and layout enforcement. In Next.js 16 running on React 19, the core image engine hasn't drastically altered its API syntax, but strict default security rules and modern browser fetch prioritization make configuration mistakes far more costly.

The sizes Attribute Isn't Optional

When you don't know the exact width and height of an image upfront—like full-width banner images or responsive card grids—you pass the fill prop. The moment you use fill without defining a sizes prop, Next.js falls back to a default value of 100vw. That single default ruins mobile performance.

Here is what happens behind the scenes: when the browser reads the srcset generated by Next.js, it looks at sizes="100vw" and assumes the image will occupy 100% of the viewport width regardless of screen size. On an iPhone 15 with a 3-column grid layout where the image actually renders at 120 pixels wide, the browser downloads a 1080px or 1920px image variant from /_next/image. You burn 600KB of payload for a small thumbnail.

To fix this, supply a media query string that informs the browser layout engine before CSS finishes parsing:

import Image from 'next/image';

export function ProductCard({ title, imageUrl }: { title: string; imageUrl: string }) {
  return (
    <div className="relative aspect-square w-full max-w-sm overflow-hidden rounded-lg bg-gray-100">
      <Image
        src={imageUrl}
        alt={title}
        fill
        sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
        className="object-cover"
      />
    </div>
  );
}

With this sizes definition, a browser rendering a 3-column desktop layout knows the image only takes up roughly 33% of the window. It selects a smaller image target from the generated srcset breakpoint array. In our testing on a media-heavy e-commerce listing page, fixing sizes across 24 product cards cut page payload from 8.4MB down to 1.1MB, reducing Largest Contentful Paint (LCP) from 3.2s to 840ms.

Fixing LCP and Layout Shift with priority

By default, Next.js sets loading="lazy" on every image. This works well for content sitting below the fold. But if your main hero image or primary product view relies on lazy loading, your LCP score will suffer. The browser delays fetching the image until after DOM layout calculation completes.

Adding the priority prop instructs Next.js to preload the image resource. In Next.js 16 with React 19, passing priority={true} does three specific things:

  • It removes the loading="lazy" attribute entirely from the rendered DOM node.
  • It injects a high-priority fetch hint (fetchpriority="high") onto the generated <img> tag.
  • It automatically renders a <link rel="preload" as="image"> tag into the HTML head during Server-Side Rendering (SSR).

Do not throw priority on every image on the screen. Network contention will stall critical JavaScript hydration bundles if five large images try to preload simultaneously. Reserve priority strictly for the single above-the-fold image driving LCP—typically your page hero or main featured card.

Configuring remotePatterns Correctly

If your application serves dynamic media uploaded from a Laravel 12 API backend running on AWS S3 or Cloudflare R2, trying to render an external URL without explicit permission breaks the build with an error: Invalid src prop on next/image, hostname is not configured under your remotePatterns.

The legacy domains array in next.config.js is fully deprecated in Next.js 16. You must use remotePatterns. The biggest security mistake developers make here is using open wildcards like hostname: '**' to bypass domain restrictions quickly. Doing this turns your Next.js application server into an open proxy, allowing anyone to request arbitrary external images through your /_next/image endpoint and drain your server resources.

Here is a tight, production-ready next.config.ts setup that handles S3 bucket subdomains, Cloudflare R2 media paths, and signed query parameters cleanly:

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'],
    minimumCacheTTL: 86400,
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'my-app-assets.s3.us-east-1.amazonaws.com',
        port: '',
        pathname: '/uploads/**',
      },
      {
        protocol: 'https',
        hostname: '*.r2.cloudflarestorage.com',
        port: '',
        pathname: '/public-media/**',
        search: '?X-Amz-Algorithm=*',
      },
    ],
  },
};

export default nextConfig;

Handling Signed URLs and Query Strings

Notice the search field in the second pattern. If your backend (such as a Laravel 12 API generating temporary URLs with Storage::temporaryUrl()) appends signature query parameters like ?X-Amz-Signature=... or ?token=..., Next.js 16 lets you match query parameters strictly. If your config specifies a search pattern, requests lacking matching search parameters will be rejected by the optimization server.

If your images fail to display with a 400 status from /_next/image despite matching hostnames, check whether query parameters are being stripped or disallowed by your remotePatterns rule. Leaving search empty matches any query parameters by default, but if you define an explicit string without wildcards, exact matching rules apply.

Backend Integration: Laravel 12 Cache Headers

When Next.js fetches an upstream remote image from an external origin (like Laravel serving uploaded media or S3 buckets directly), it respects the upstream origin's Cache-Control header. If your Laravel backend or S3 origin sends Cache-Control: no-cache or max-age=0, Next.js will re-fetch and re-optimize the full source image on every single request, completely bypassing its internal disk cache.

In Laravel 12 controller actions serving binary files with PHP 8.3, ensure your response headers grant long cache lifespans for immutable uploads:

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

class MediaController extends Controller
{
    public function show(string $path): BinaryFileResponse
    {
        $filePath = storage_path('app/public/uploads/' . $path);

        return response()->file($filePath, [
            'Cache-Control' => 'public, max-age=31536000, immutable',
        ]);
    }
}

Setting max-age=31536000, immutable ensures Next.js optimizes the asset once and serves it instantly from its local build cache or CDN edge for subsequent hits.

Avoiding Common Layout Shift Pitfalls

Layout shift happens when the browser allocates zero space for an image while fetching it, forcing surrounding text and elements to jump once dimensions load. Prevent CLS with these three core rules:

  1. Always provide aspect ratio containers for fill images: Use Tailwind classes like aspect-square, aspect-video, or explicit inline style dimensions on the parent element. A parent container with no height forces fill images to collapse to 0px height initially.
  2. Do not mix width/height with CSS percentage styles directly without height auto: In React 19, setting raw width={500} height={300} sets the element's intrinsic aspect ratio. If you stretch the image via CSS width, add height: auto so the browser scales height proportionally without distortion.
  3. Use blur placeholders for remote assets carefully: Setting placeholder="blur" on local imports generates inline Base64 data automatically. For remote images, you must provide a valid Base64 blur Data URL to blurDataURL manually—otherwise Next.js throws a runtime error.

Getting image optimization right comes down to being explicit. Define your remote patterns tightly, tell the browser how wide your images render across viewports with sizes, and let Next.js handle formatting and caching.

Md. Mostafijur RahmanMMd. Mostafijur Rahman

WRITTEN BY

Md. Mostafijur Rahman

    Latest Posts

    View All

    Next.js Image Optimization: Sizes and Remote Patterns

    Next.js Image Optimization: Sizes and Remote Patterns

    Next.js Route Handlers vs Server Actions: The Real Boundary

    Next.js Route Handlers vs Server Actions: The Real Boundary

    Optimizing nextjs generatestaticparams at Scale

    Optimizing nextjs generatestaticparams at Scale

    Refactoring Laravel Fat Controllers Without Over-Engineering

    Refactoring Laravel Fat Controllers Without Over-Engineering

    Laravel Action Classes & DTOs: Clean Code vs Ceremony

    Laravel Action Classes & DTOs: Clean Code vs Ceremony

    Safe, resumable data backfills for Laravel

    Safe, resumable data backfills for Laravel

    Query Builder vs Eloquent: Real Cost in Laravel

    Query Builder vs Eloquent: Real Cost in Laravel

    Laravel Signed URLs and One-Time Download Links

    Laravel Signed URLs and One-Time Download Links

    Laravel Timezone Handling: UTC, Users, and DST Bugs

    Laravel Timezone Handling: UTC, Users, and DST Bugs

    Laravel Login Throttle: Rate Limiting and Credential Defense

    Laravel Login Throttle: Rate Limiting and Credential Defense