Skip to content

Next/Image with Supabase Storage: The Pattern That Saves 70% of Hero Image Bandwidth

Most teams either skip next/image (and ship 4MB heroes) or misconfigure it (and break Coolify deploys). Here’s the pattern that works.

John Cravey with AIFounder4 min readUpdated Sep 11, 2026

next/image is the single biggest perf lever in a Next.js app you’re not using. It auto-resizes, auto-converts to WebP/AVIF, and serves the right size for the device. The catch: it has to know the source host, and if your images live in Supabase Storage, that means configuring `images.remotePatterns` correctly. This is the pattern we use on every FH client site.

Free estimate · 2 minutes

Slow pages lose rankings and leads. Price the fix.

Build the estimate to see what a fast rebuild would look like for your site and what we would ship in the first thirty days.

Why next/image earns its keep

A homepage hero from an unoptimized JPEG is typically 800KB–2MB. The same hero served by next/image in AVIF at the right resolution for the device is 60–180KB. On mobile that’s the difference between a 2.4s LCP and a 4.1s LCP. Core Web Vitals are a ranking factor, so this isn’t cosmetic.

Step 1: configure remotePatterns

next/image refuses to optimize any URL whose host isn’t allowlisted in `next.config.ts`. The Supabase storage URL pattern is `<project-id>.supabase.co/storage/v1/object/public/<bucket>/<path>`. Allowlist it.

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

const config: NextConfig = {
  output: "standalone",
  reactCompiler: true,
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "iymmsrexwwqwilmbpvna.supabase.co",
        pathname: "/storage/v1/object/public/**",
      },
    ],
  },
};
export default config;

Step 2: use a typed URL helper

Don’t hand-roll Supabase URLs. They break when buckets get renamed. The FH `imageUrl()` helper in lib/storage.ts is the single source of truth — pass it a bucket-relative path and it returns a fully-qualified URL. If the bucket changes, one file changes.

// lib/storage.ts
export function imageUrl(path: string, opts: { bucket?: string } = {}) {
  const bucket = opts.bucket ?? "fh-images";
  const cleaned = path.replace(/^\/+/, "");
  return `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/${bucket}/${cleaned}`;
}

Step 3: use Image with explicit width/height

next/image needs width and height to reserve layout space — otherwise you get Cumulative Layout Shift when the image loads. Use the natural dimensions of the source image, or the rendered dimensions if you’re using `fill`.

import Image from "next/image";
import { imageUrl } from "@/lib/storage";

export function HeroImage() {
  return (
    <Image
      src={imageUrl("home/hero.jpg")}
      alt="FH team at work"
      width={2400}
      height={1350}
      priority
      sizes="100vw"
      className="w-full h-auto"
    />
  );
}

Step 4: priority on the LCP image, lazy on the rest

Set `priority` on the single image most likely to be your Largest Contentful Paint candidate — usually the homepage hero. That image gets preloaded. Every other image stays lazy by default. We use `priority` on exactly one image per route.

Step 5: get sizes right

The `sizes` prop tells next/image which width to serve at each breakpoint. For a full-width hero, `sizes="100vw"`. For a half-width image at desktop, full-width on mobile, `sizes="(min-width: 768px) 50vw, 100vw"`. Getting this wrong means serving a 2400-wide hero to a 360-wide phone — 6x the bytes for no visual benefit.

Bucket organization across FH clients

We use one Supabase project for the entire client book, with a bucket per client (`fh-images`, `cab-images`, `tivey-images`). Each bucket is public-read, service-role-write. RLS is `site_id`-scoped so a client’s site can never accidentally read another client’s bucket through the API layer. The `imageUrl()` helper takes a `bucket` option for shared components.

Step 6: pre-process images before upload

next/image will resize at request time and cache the result. But it won’t fix a bad source. If your source image is a 14MB raw export, the first request to optimize it can time out on small Coolify VPSes. Pre-process: resize to 2400px max width, save as a high-quality JPEG, and let next/image handle the rest. We use a simple Sharp script in our content pipeline for this.

Cache control on optimized images

next/image caches optimized variants in `.next/cache/images`. By default the cache TTL is 60s for remote images, which is too short. Set `minimumCacheTTL` to a longer value to keep the cache warm.

images: {
  minimumCacheTTL: 60 * 60 * 24 * 30,   // 30 days
  remotePatterns: [/* … */],
}

When to fall back to a plain <img>

Two cases. SVG icons inline — next/image is wasted on SVG, just inline it or use a plain `<img>`. And third-party images you don’t control (a partner logo from a CDN you don’t own) — those need to be allowlisted in remotePatterns, which spreads your trust boundary. For untrusted images, use a plain `<img>` with `loading="lazy"` and accept the bandwidth cost.

Verifying the win

After wiring next/image, open the Network tab in DevTools and look at the homepage hero. You should see a URL like `/_next/image?url=...&w=1920&q=75` with a content-type of `image/avif` or `image/webp`. The file size should be a fraction of the original. Run PageSpeed Insights and confirm LCP dropped.

Pulling it together

next/image + Supabase Storage + the `imageUrl()` helper is the FH image stack. Forty lines of config, one helper, and you’re shipping right-sized images on every page. If your site is still serving raw JPEGs or has next/image misconfigured, book a consultation — this is the kind of focused engagement that ships in 3 days and pays back in measurable ranking lift over the next quarter.

Answers

Frequently asked questions

Why does next/image earn its keep?

It resizes, re-encodes to modern formats, and serves the right variant per device, which routinely cuts hero bandwidth dramatically against an unoptimized upload. It also reserves layout space, which removes a common source of layout shift. Both effects show up in Core Web Vitals.

Why do my bucket images fail to optimize?

Because the storage host is not in the image config's remote patterns, so the optimizer refuses it. The symptom is either an error or a silently unoptimized image, and it is the single most common misconfiguration when images move from the public folder to a bucket.

Should I build image URLs by hand?

No. Hand-rolled public URLs spread the storage path across the codebase, so a bucket rename or a route change means finding every string. A typed helper keeps it in one place, which is the difference between a five-minute change and an afternoon.

Why does width and height matter if the image is responsive?

Because they establish the aspect ratio the browser reserves before the image arrives, which prevents layout shift. Responsive sizing still works; the explicit dimensions are about geometry rather than display size, and omitting them is a common cause of a poor visual-stability score.

What does priority do and where should it go?

It preloads the image and opts it out of lazy loading, and it belongs on exactly one image per page: the largest one visible on load. Marking several defeats the purpose by competing for bandwidth, and marking none leaves the main image queued behind everything else.

Why does the sizes attribute matter?

Because without it the browser assumes the image is full viewport width and downloads a much larger variant than a grid thumbnail needs. Getting sizes right is often a larger saving than any encoding change, and it is the setting most projects leave at the default.

Should images be processed before upload?

Yes. The optimizer resizes and re-encodes but cannot undo a badly cropped or wildly oversized original, and every request pays for the work. Uploading a sensibly sized master keeps the optimizer's job cheap and the results predictable.

How should buckets be organized across multiple client sites?

One bucket per client with access scoped per tenant, so a misconfiguration exposes one client rather than all of them. Service-role keys stay server-side. The organizing principle is that the blast radius of a mistake should be one tenant.

What cache headers should optimized images have?

Long-lived, because the optimized variants are content-addressed and safe to cache aggressively. Short cache lifetimes here mean re-doing the optimization work repeatedly, which shows up as slow first paints on repeat visits and unnecessary compute.

When should I use a plain img tag instead?

For SVGs, for tiny decorative icons, and for anything where the optimizer adds latency without saving bytes. The component is the default, not a rule, and forcing it onto every image produces cases where the machinery costs more than the image did.

How do I verify the optimization is actually happening?

Load the page and check what was transferred: format, dimensions, and size per image. If the bytes match the original upload, the image is bypassing the optimizer, which usually means a configuration miss rather than a component misuse.

Does this affect SEO or only performance?

Both, because the performance is the SEO. Largest Contentful Paint is usually a hero image on a marketing page, and it is a measured input to page experience. Getting image delivery right is one of the few changes that moves rankings and conversion with the same work.

Question we did not answer? Ask us directly and we will answer it here.

John Cravey, Founder
Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Newer post
Google Analytics 4 Consent Mode v2: The Implementation That Doesn’t Break Your Data
Older post
View Transitions API & Scroll-Driven Animations
Keep reading

More from the blog

Next.js·10 min

Speed Is a Ranking Factor: The Next.js Performance Checklist by Business Size

Google measures your speed with real users and ranks you on it. Next.js hands you the tools to win. Most sites leave them switched off.

Next.js·6 min

Next.js 16.1 in Production: The Migration Playbook We Run on Every FH Site

Next 16.1 is the lean target. Here’s the exact migration we run, what breaks, and what to delete after.

Next.js·4 min

Server Components vs Client Components: The Mental Model That Stops You Reaching for ‘use client’

Most teams add ‘use client’ because they’re scared. The bundle pays for it.