Oluwafemi Obajuluwa

Oluwafemi Obajuluwa › FastChow

Oversized images

14.1 MB of logos down to 48 KB

Oversized images

One thing was not render work. My order history runs to 928 orders, and scrolling it stuttered. I went through the usual suspects first and got nowhere, so I took the vendor logos out of the row and scrolled the whole thing again. Nine hundred rows, completely smooth. It was the images.

So I measured what each row was actually downloading. Every logo is served straight from files.chowdeck.com, at whatever size it was uploaded at, and some of them are enormous. These are the nine on the first page of my history, all of them drawn into a 44pt tile:

  • 6000×4000 jpg: 11.3 MB → 5 KB
  • 1132×1666 png: 2.5 MB → 6 KB
  • 342×333 png: 216 KB → 8 KB
  • 389×300 png: 185 KB → 3 KB
  • 200×300 png: 129 KB → 17 KB
  • 300×300 png: 131 KB → 11 KB
  • 300×300 png: 32 KB → 2 KB
  • 298×300 png: 20 KB → 1 KB
  • 300×300 png: 11 KB → 1 KB

Some were 24-megapixel photos being shown in a tiny 44pt image. A 3× screen only needs about 17,000 pixels for that, but these images had around 24 million. Every row was downloading about 169× more image than it needed. At 60fps, the app has just 16ms to render each frame, and decoding an image that large can use up most of that time on its own.

The original Chowdeck app never trips over this, because you scroll past ten orders and stop. Mine scrolls hundreds of them, so it hits every vendor in the space of a few seconds.

There is no resizing on the API side. The files sit on plain storage, and every resizing parameter I tried came back byte-identical to the original. So I put a small Cloudflare Worker on my own domain. It takes an image URL and the size the app is about to draw it at, fetches the original once at the edge, and returns it resized and re-encoded as AVIF or WebP depending on what the device accepts. Cloudflare caches the result, so each image is only ever processed once.

The same page of rows went from 14.1 MB of images to 48 KB. The 11.3 MB logo comes back as a 5 KB AVIF. Requests are checked against a list of known image hosts before anything is fetched, and if the Worker cannot be reached the app loads the original image instead, so a proxy that goes down costs speed rather than a broken screen.

Two smaller things came out of the same work. Images now ask for the size they are actually rendered at rather than whatever the file happens to be, and sizes are rounded into buckets so a 131pt and a 133pt layout do not generate two near-identical images. And the app reuses a decoded image by its URL rather than by the row it appeared in, so the six orders you placed at the same restaurant share one decode instead of doing six.