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 EleviFounder4 min read

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.

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.

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 and CSS Scroll-Driven Animations: The Browser Wins of 2026
Keep reading

More from the blog

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.

Supabase·5 min

Supabase Storage for Marketing Sites: The Bucket-Per-Tenant Pattern

Most teams store images in their build artifact. That doesn’t scale. Supabase Storage with the right bucket layout does.