🚀 Speed Up Your Next.js App: Optimizing S3 Images with Cloudflare Images

Let’s talk about something near and dear to every developer’s heart: making stuff faster without tearing your hair out.
So you’ve got your images chilling in an S3 bucket — classic move. But if you’re serving them straight from there, you’re probably pushing chunky, uncompressed images across the internet like it’s 2008. On the flip side, maybe you’re leaning on Next.js’s built-in image optimization… which is cool until your server starts wheezing under the load like it just ran a marathon.
But don’t worry — there’s a better way. In this post, we’ll walk through a dead-simple setup using Cloudflare Images to optimize and cache your images right at the edge, leaving your compute power untouched and your pages blazing fast.
🧰 What You’ll Need
Before we dive in, make sure you’ve got the basics:
- An AWS account (for your S3 bucket full of glorious JPEGs). If you're using something else for file storage, that’s totally fine too — we don’t judge.
- A Cloudflare account — free tier works great.
- A domain hooked up to Cloudflare.
- And since this post is all about Next.js, we’ll assume you’ve already got that part set up and humming along. If not… this might be a weird place to start.
🛠️ Setup Guide: The Part You’re Actually Here For
Time to get our hands slightly dirty (but like… not real dirty — this is all pretty painless). Let’s hook everything up so Cloudflare can work its optimization magic on your S3 images.
🪣 1. Set Up Your Bucket
If you already have an S3 bucket full of images, awesome — you’re halfway there. If not, go ahead and create one. Give it a nice name, something you won’t be embarrassed about later — you’ll be seeing it a lot.
Now the important part: Cloudflare Images needs to be able to access your images. If you’re already serving images from S3, you’ve probably handled this with either public access or presigned URLs. If not, here’s the quick-and-dirty setup:
✅ Make the bucket public (yes, really)
- Go to your bucket permissions
- Make sure “Block public access” is turned off
- Then slap this bucket policy on there to allow public reads (it’s just below “Block public access”:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::<your-bucket-name>/*" } ] }
Don’t forget to swap in your actual bucket name, unless you want Cloudflare to look for <your-bucket-name> and come back very confused.
That’s it — your images are now ready to be fetched and optimized like champs.
🌩️ 2. Next stop: Cloudflare land (a.k.a. Where the Magic Happens)
- Head to your Cloudflare dashboard, choose your domain, and go to the Images → Transformations tab. Click the little “Enable” toggle next to your domain.
- Still on the same page, click your domain to open the settings. Under Sources, select Specified Origins and add your S3 bucket’s public domain (like https://your-bucket.s3.eu-west-1.amazonaws.com). You can select “Any origin” here… but unless you enjoy living dangerously, stick with the specified option.
3. 🌈 Cloudflare Image Format — Fancy URLs That Do All the Work
Cloudflare’s image transformation magic works through simple (and surprisingly readable) URLs. The format looks like this:
https://<your-domain>/cdn-cgi/image/width=<width>,quality=<quality>,format=<format>/<S3-image-url>
Let’s break that down:
- width – how wide you want the image in pixels (e.g., 800)
- quality – how much quality to keep (1 to 100 — 75 is a solid sweet spot)
- format – choose from auto, avif, webp, or jpeg. Just go with auto unless you’re feeling experimental
- S3-image-url – the full URL to the image in your bucket, like:
https://bucket-name.s3.eu-west-1.amazonaws.com/awesome-cat.jpg
Here’s a complete example:
https://your-domain.com/cdn-cgi/image/width=800,quality=75,format=auto/https://bucket-name.s3.eu-west-1.amazonaws.com/awesome-cat.jpg
Cloudflare does all the heavy lifting — resizing, compressing, and even converting to modern formats. Your users get fast, lightweight images, and your server gets to nap. 😴
Want to get fancy? There are more transformation options here, but width, quality, and format cover 90% of what you’ll need.
4. 🧩 Plugging It Into Next.js
Alright, time to wire this into your Next.js app. I’m going to assume you’re using the <Image>
component from next/image
— because if you’re not, now’s a great time to start.
🥾 Option A: The Quick and Dirty Way
The quick-and-dirty method: just feed the transformed Cloudflare URL directly into the <Image> component.
import Image from "next/image" <Image src="https://your-domain.com/cdn-cgi/image/width=800,format=auto/https://bucket-name.s3.eu-west-1.amazonaws.com/cat-wearing-a-crown.jpg" width={800} height={500} alt="A regal cat wearing a golden crown" />
Done. It works. Your cat is now optimized.
🧼 Option B: The Cleaner, More Reusable Way (Custom Loader)
If you want to keep your code tidy and avoid sprinkling Cloudflare URLs everywhere, you can roll a custom image loader.
First, update your next.config.js:
module.exports = { images: { loaderFile: "./cloudflare-loader.ts" }, }
And if you’re using remote images, you’ll probably need to add a remote pattern too:
remotePatterns: [ { protocol: "https", hostname: "bucket-name.s3.eu-west-1.amazonaws.com", }, ]
Then, create a new file called cloudflare-loader.ts
in your project root:
import { ImageLoaderProps } from "next/image" const BUCKET_DOMAIN = "bucket-name.s3.eu-west-1.amazonaws.com"; const CLOUDFLARE_DOMAIN = "<your domain>"; export default function cloudflareImageLoader({ src, width, quality = 75, }: ImageLoaderProps): string { try { if (!src.startsWith("/")) { const parsedSrc = new URL(src) if (parsedSrc.hostname === BUCKET_DOMAIN) { const params = [`width=${width}`, `quality=${quality}`, "format=auto"] return `${CLOUDFLARE_DOMAIN}/cdn-cgi/image/${params.join(",")}/${src}` } } } catch (error) { console.error("Cloudflare image loader error:", error) } // Fallback to Next.js built-in image optimization return `/_next/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}` }
This setup:
- Applies Cloudflare optimization only to images from your S3 bucket
- Falls back to Next.js optimization for everything else
- Keeps your JSX clean and your dev brain happy 🧠✨
🎉 Result? A Happier, Faster App
Now you’ve got:
- On-the-fly image optimization
- CDN delivery from Cloudflare’s edge nodes
- No need to pre-process or move your images
- Better scores in Lighthouse, Core Web Vitals, and all that jazz
🧋Wrapping Up
Optimizing images is one of the lowest-effort, highest-impact improvements you can make to a modern web app. And thanks to Cloudflare Images + your trusty old S3 bucket, you don’t need to rearchitect your whole stack to do it.
So go ahead, optimize those cat pictures. Your users — and their data plans — will thank you. 🐱💨