When serving images from S3 stopped being good enough
Source: Dev.to
Background
I published my first blog post 7 years ago. I wrote on Medium for about a year before I built Ready, Set, Cloud. For most of its life the site hasn’t had much of a facelift or performance updates – it’s primarily served as a home for my writing, newsletter, and podcast.
I’ve been running this site for six years. I’m usually the kind of person who can’t leave things alone for long, yet this site was largely unchanged for years. It worked… until it didn’t.
The Performance Problem
When I finally took a look at site performance, something was painfully obvious: images.
- PageSpeed Insights showed that large, unoptimized images were dominating load time.
- Images were served as single files directly from S3, with no format negotiation and no caching.
That setup wasn’t wrong when I built it. It was a perfectly reasonable trade‑off at the time: simple, low‑maintenance, and “good enough” for a couple‑dozen readers. But as the site grew—both in content and audience—performance expectations changed. Serving raw images straight from S3 no longer cut it.
Why a Simple Fix Isn’t Enough
A knee‑jerk reaction would be to jump into S3 and manually optimise a bunch of images. That fixes a symptom, but it doesn’t solve the underlying problem.
- Manually resizing images, converting them to web‑friendly formats, and deciding which size to upload per post won’t scale.
- I didn’t want to change my workflow. I wanted to keep writing, publishing, and moving on without adding layers of performance checking.
A good system should:
- Optimize images automatically in the background.
- Serve modern formats like WebP when the browser supports them.
- Provide multiple image sizes so mobile devices aren’t downloading desktop‑sized assets.
- Be aggressively cacheable—CDNs already solve image delivery exceptionally well.
Rethinking Image Handling
I realized I wasn’t looking to build a new image‑optimisation tool (as I might have done in the past). Instead, I needed to centralise the optimisation decisions in an industry‑standard way.
Upload Script
I upload images to Ready, Set, Cloud using a small JavaScript script that lives in the repo. It:
- Takes a file prefix.
- Scans a local folder for matching files.
- Uploads them directly to S3.
There’s no web UI and no automation that tries to interpret content—it’s simple by design, and I wanted to keep it that way.
Automatic Processing
Because images already flow through S3, the system can react to uploads without changing the workflow at all.
- Image uploads automatically trigger a Rust Lambda function (via EventBridge).
- The Lambda converts the original image to WebP and generates a handful of standard sizes.
- Optimised versions are written back to S3 alongside the original.
Benefits
- Deterministic optimisation – every image goes through the same process every time.
- Work off the critical path – all of this happens asynchronously, so publishing isn’t delayed.
CDN Delivery
Once processing was handled, delivery became the next concern. All optimised assets were already in S3, but I didn’t want to change 2,000+ image links across existing content.
Content‑Negotiation at the Edge
The solution was to push the behaviour into the CDN:
- CloudFront distribution sits in front of the S3 bucket.
- It looks for WebP support in the
Acceptheader. - If the browser advertises WebP, a CloudFront Function rewrites the request path to the WebP version.
The function runs on the viewer‑request event and rewrites the path without checking if the WebP file exists—that check happens later when CloudFront fetches from the origin. The rewrite is consistent at the edge, and the resulting response is cached, so subsequent requests follow the same path.
Serving the Right Size with srcset
Having multiple sizes of every image automatically raised the next question: which one to serve?
- Mobile screens don’t need desktop‑sized images.
- High‑DPI displays can benefit from larger ones.
- Thumbnails on the home page should be as small as possible.
srcset in Action
Using srcset lets the browser choose the appropriate image based on viewport and device pixel ratio. The markup stays the same, there’s no runtime logic, and each client downloads only what it actually needs.

Ready, Set, Cloud is a static site generated by Hugo. Adding srcset support meant overriding the render-image hook and adding a small amount of logic to create the additional attribute.
Outcome
My primary goal was to decrease load times so the site feels snappy without adding anything to my existing workflow.
- Image optimisation (Rust Lambda) runs automatically on upload.
- CDN delivery (CloudFront + edge function) serves the right format and size, cached globally.
srcsetlets browsers pick the optimal asset, eliminating the need for manual link updates.
The result is a faster, more scalable site that continues to work with the same simple publishing process I’ve always used.
Performance Improvements
I added an IDE srcset addition, and I’m pleased to say page payloads dropped significantly. Pages render faster, Largest Contentful Paint (LCP) improved, and overall performance is more predictable.
- On my homepage, the First Contentful Paint (FCP) dropped from 4.5 seconds to under a second.
- Load times are much more consistent across desktop and mobile as well, which has been an ongoing challenge.
SEO Bonus
The unexpected bonus was that these same changes also helped with search engine rankings. Faster pages, smaller downloads, and aggressively cached assets all feed into Core Web Vitals. So, without targeting SEO explicitly, the site became easier to crawl, faster to index, and rank higher in search results simply by prioritising user experience.
Common Bottlenecks
If you’ve built your own blog, you’ve probably run into these performance bottlenecks just like I have, and that’s okay. Serving assets out of S3 is a valid solution and has worked well for me for six years as the site grew.
Extending the System
When my constraints changed, I wanted to extend the system without changing the deployment workflow.
- If you’re interested in the same improvements I’ve described here, this setup is available in the Serverless Application Repository as a drop‑in addition to an existing system.
- The full source is also available on GitHub if you want to dig into the details or adapt it further.
Keeping Decisions Out of the Day‑to‑Day Workflow
Above all else, this was about taking an entire class of decisions around image formats, sizes, and caching, and keeping them out of the day‑to‑day workflow. With everything in place, performance simply happens by default.
Closing Thoughts
This was as fun as it was important for Ready, Set, Cloud. I wanted something easy to adopt, easy to remove, and something that quietly does the right thing as everything else moves around it.
Happy coding!