Dinner.png is Served
How to Optimize your Image-Handling with Cloudflare Workers
A Common Conundrum
So, you’re building the Next Great App, and you’ve got a folder full of gorgeous, high-resolution images you’re eager to use. You’ve also got a big decision to make: where will those images live? You could store them locally in-app — they’ll certainly load quickly and reliably. But what happens as your app grows? Each image you add will push you closer to app store size limits, while the ever-increasing download size of the app will become an impediment to potential users with older mobile devices. This tale of woe is rather commonplace among application developers — in fact, we recently found ourselves in this precise pickle at Propeller.
Fortunately, the question “How can we handle oodles of images in a sustainable way?” has been asked by so many people that I found myself spoiled for choice regarding potential solutions. My bare-minimum needs were simple: I wanted to be able to serve images from an S3 bucket via a configurable URL which would allow clients to dynamically specify image attributes such as dimensions and format.
A Spiffy Solution
After snuffling through AWS’s more familiar offerings, I decided to explore a new-to-me realm: the Cloudflare cosmos. Now, I’m a disciple. Cloudflare workers are uncomplicated, effective, and highly configurable. Inbound knowledge is not a prerequisite for success; Cloudflare’s documentation provides an extensive collection of worker tutorials as well as a playground in which you can immediately begin exploring sample code without any setup. Their Wrangler CLI tool makes it easy to deploy a starter project locally and begin building without any hullaballoo whatsoever.
Cloudflare’s image resizing capabilities are best defined by its long list of accepted query parameters which can automagically resize, reformat, sharpen, or rotate any image that you request. It can even strip an image of its metadata, add an overlay, or apply background colors to images with transparency. All you need to do is store your images somewhere that Cloudflare can find them and write a little bit of code in one of Cloudflare’s supported languages.
Here’s a nutshell play-by-play of how my worker runs:
- I manually upload an image to a private S3 bucket which Cloudflare has permission to access
- I request the image in a browser via one of the Cloudflare domains defined in my wrangler.toml configuration file; i.e. https://images.mydomain.com/images/s3_path/imageStoredInS3.png
- Handle that request via some simple worker code(more on this in a minute)
- Happy path: the resulting image is served -or- Unhappy path: the resulting error is handled
And there you have it: Presto-Changeo, optimized images on-demand — and all of it can be accomplished in very few lines of code. For proof, let’s step through my jumping-off point.
A Tiny Tutorial
First, you’ll need to set up your worker to handle requests.
To build out the handleRequest() function to transform the request you received into a request to Cloudflare’s image-optimization service:
1) Access the query string by parsing the request URL
2) Collect any passed-in query parameters for image optimization
3) Stash those parameters in the cf object (this is how they must be submitted to Cloudflare for consideration)
Next, re-build the image URL to point at your source image’s actual location in S3, prepare the request to Cloudflare, and fetch the optimized image.
Of course, this is just a bare-bones shell, designed to be configured. So, how did my worker evolve over time? I’m glad you asked!
I needed to account for some formatting defaults; I knew that both mobile and web clients would end up using my image-resizing service and I wanted to ensure that the web clients would receive .webp images automatically when there was browser support for them. I ultimately built a custom autoserveWebp() function based on a code snippet I stumbled upon in the Cloudflare Community forum that was easy for me to adapt to my needs.
Constructing a function to generate a default format for certain web clients got my gears turning; perhaps it would make sense to factor in a couple more? Therefore, I chose my favorite fit and metadata parameters and slotted them into my code as well.
Now, by default, my worker will strip optimized images of all metadata (metadata: “none”) and ensure that aspect ratio is preserved when both width and height are specified in the query parameters (fit: “contain”). Of course, all of these defaults can be overridden by parameters passed in by the client; I may be a bit of a backseat driver, but I don’t have a God complex.
Up until now, it’s been assumed that an image will always be available at the destination URL, or that the response from Cloudflare will always be a fair-weather friend. But what about the inevitable treks down the unhappy path? As per usual, Cloudflare’s docs toss out a couple of good options which you can easily mix and match. After polling my colleagues for opinions I ended up returning a 404 by default but also setting up a custom query parameter of my own along with a default image generation engine that will return appropriate default images from S3 if the frontend clients ask nicely.
Around this time, I realized that my worker had become rather unwieldy, so I had a good old post-programming hack-and-slash fest to modularize my codebase a bit, and because I built my worker in Javascript, I had Webpack at my disposal for bundling. Cloudflare does have its own documentation on implementing Webpack with workers, but I also found this 2018 tutorial by John Fawcett to contain some helpful extra context.
Honesty is the Best Policy
Now that you’ve seen the sunny side, let’s talk about a couple of tiny elephants in the room. Firstly, while most of Cloudflare’s offerings are available to all of its customers, image resizing is only available to those with Business and Enterprise accounts. Because of this, when you deploy your code locally you can display images from S3 in your browser, but you can’t actually see the resizing/reformatting occur unless you publish your worker to the web, where it’ll use your official Business/Enterprise account DNS. This makes for a mildly annoying development cycle, but at least there’s a handy GitHub Action to make it easy to deploy code to Cloudflare straight from a repo.
Also, while building a serverless image handler makes image storage much simpler client-side, it introduces its own Big Questions. If you have workers deployed to multiple environments, should you also maintain multiple image buckets, or will that just muddy the waters? Who will be responsible for adding images to the bucket(s)? What will the verification and validation process for that be like? It’s when I’m faced with questions like these that I truly appreciate the iterative nature of software engineering —there’s a comforting amount of freedom in the create, rethink, rework, repeat cycle.
Optional Afterthoughts
Throughout this project, I grossly benefited from documentation of all sorts — both official and crowd-sourced. I wasn’t the first person at Propeller to dig into this problem, and Propeller was far from the first company to attempt to solve it in the first place. This blog post is my thank-you note to those that paved the way for me. By publicly reflecting on some of my informed decisions here, I might one day help someone else make a few of their own.
Finally, even if you don’t need an image-handling service for your application, I hope that you will consider Cloudflare’s offerings and tutorials the next time you’re looking for a serverless solution to your problem (if you haven’t already). If nothing else, it’s a stellar example of how to write documentation that sets a low barrier to entry for a product — and for $0.00, I think that’s a steal.