Twelve megapixels of dog
Shelter staff upload photos straight from a phone camera. We were paying to move 8MB files across a bad connection to produce a 400px thumbnail.
In January our image bill was 41% of total infrastructure spend, which for an app with 63 shelters and a five-figure listing count is absurd. The cause was not storage. It was that we were transferring full-resolution camera output over the worst connections we have, then discarding 97% of the pixels server-side.
The numbers before
median upload 7.9 MB (4032×3024, HEIC or JPEG)
median upload time 48 s (shelter Wi-Fi, p50)
p90 upload time Failed (timeout at 120 s)
largest served variant 1200 px
bytes actually used ~2.4%The p90 line is the one that mattered. Nearly one upload in ten never completed, and a failed photo upload means an animal listed without a picture, which means a listing nobody taps.
Downscale on the device
The fix is unglamorous: resize before uploading. We downscale to a 1600px long edge and re-encode at quality 82 on the phone, which takes about 300ms on the slowest device in Máté's drawer and turns an 8MB upload into roughly 340KB.
const resized = await ImageManipulator.manipulateAsync(
uri,
[{ resize: { width: 1600 } }],
{ compress: 0.82, format: SaveFormat.JPEG }
);
// keep the original only if the device is on wifi *and* charging
if (await shouldKeepOriginal()) queue.enqueue(originalUpload(uri));We do keep originals, because a shelter photo is sometimes the only record of an animal's condition at intake and staff have asked for them in disputes. They upload opportunistically — on Wi-Fi, while charging, and never in a way that blocks the listing going live.
Serving
Four variants, generated once on upload rather than on demand: 400, 800, 1200, and 1600px, AVIF with a JPEG fallback. On-demand generation had been a source of cold-start latency on exactly the images most likely to be seen first — a newly listed animal, whose thumbnail nobody had requested yet.
Every image transform you can do once at write time is a transform you are otherwise doing thousands of times at read time, on the critical path, for free, forever.
After
- Image spend down 74% month over month.
- p90 upload time 6.1s, down from a timeout.
- Listings created without a photo fell from 11% to under 1%.
- Listing page LCP on 3G improved by 1.9s, which we did not expect and will take.