Wave

Effectively Serving a Large Amount of Images

Mia Rose Winter

When you build an app, especially a web app, you will inevitably think about how to serve the user your images. In a static setting, that's easy, put them into a folder and add a relative link to it in your page, maybe with an async flag if you are fancy, done, the browser does the rest. But in a dynamic setting, you don't have that luxury, you usually want users to upload images… and now you need to decide how those images get to your server, and later, get to every visitor needing it.

Image processing

first, some general advice. When people can upload images, you first have to ensure their security. Never just put their upload anywhere reachable by outsiders, it may be a JPEG with Exif data containing the location the image was shot, the people in it and that's a big privacy no-no. You also need to protect your other users, whatever just got uploaded may, somehow, alter the behavior of your page or even execute malicious code (hello GitHub). Lastly, there is also the aspect of efficiency. When a User uploads an 8k PNG, that's gonna take someone on a phone with a 3G connecting literally minutes to fetch, so you want to downsize images that are too large, and save them in an efficient, streamable, format, like JPEG with high compression or WEBP.

For an example: Wave enforces a maximum size of 800x800 for profile pictures, and encodes them as JPEG with 90% compression. This introduces a side effect, that depending on your use-case might be an upside or a downside, and that is that it removes transparency, since JPEG doesn't have that. In case of Wave, when you upload a PNG with transparency, the transparent parts will be changed to black, a desired effect for me, since a lot of my layout relies on the square nature of profile pictures.

And of course, don't forget appropriate cache headers, so browsers don't keep requesting your company logo on every request.

Static with extra steps

Of course, the most simple solution is to do just what a static page did, store the images into a folder of your web server, and then link to it. Perfectly fine to be honest, and you may even able to enable some response caching with your load balancer. This does mean however, that you need to track what images there are, and what they are used for, but with a database that is easy enough, store the path to it, and insert it into the page where needed, and hope whoever has access to the server never touches your image folder (I call this the Matrix approach). Sadly, it has downsides, like when the image disappears, then you are just gonna 404 on that one, with just your load balancer reporting that something is amiss. It also makes backups more complex, since now you also have to back up this folder. This solution is also not very scalable, since every image is a web request the server needs to handle, and if you don't process the images into proper formats with proper sizing, a full page load may take a while.

The Web Api

Instead of serving your images “simply from the server”, you might instead opt to serve them over an API. In this case you would read the image in yourself, which may still be a file location, but could be anything that can hold bytes really, and answer the user with those bytes and a content type of image/jpeg for example. That alone already allows you to choose the method of storing your images much more easy, like you could use some sort of S3 service, store it more effectively on file using atlases, or even put them into a database (only do that one when they don't exceed 4kb, and you don't mind half the web dev community wanting to hurt you (thumbnail databases are perfectly valid)).
This will most certainly also allow you to perform automated output-caching, since most web frameworks support that. Output-caching means, that whenever an endpoint is queried, the framework checks if it hasn't been queried recently in a similar manner, and responds with data from cache instead of hitting your API again. This alone can already provide performance benefits for frequently used images and images with slow access, like Waves' little favicons on “About the Author” links, since they are fetched from an external API with a relative slow response time. It is also good manners to not hit someone providing an API to you for free with constant requests for the same data, so in a case like this output-caching might be a must-have for you.
You can now also make this even more awesome, if you allow your images to be processed before they are requested. Most commonly, you don't always want the image at full resolution, I mentioned before, profile pictures in Wave are 800 by 800 pixel, but the image on the bottom right of the page when logged in will be much much smaller, usually around 20 to 100 pixels. So the endpoint for profile pictures allows to request a size, the endpoint then scales the image down to that size before answering. Together with output caching this allows for extremely high performance, since now little images are just processed every now and then, have a small transmission size, and thus is essentially always available immediately, they might even look better depending on your resize algorithm.

About file access

As previously mentioned, putting your images in a folder is the easiest scenario, but also the slowest. Accessing a file on hardware is, in computer time, extremely slow. The reason for that is multi-layered. First, there is a lot of housekeeping and bookkeeping, an operating system has to ensure you actually have access to this file, it needs to provide you with pointers to the memory blocks of that file, and it needs to keep track where you write to, so no other program accesses the same place and mangles the file. For this reason you should always tell the OS that you are accessing a file only for reading if possible, since then it has to worry less about a lot of stuff, especially if the image isn't opened for writing by any process. The next problem is logistics, when reading a file, that file is on a hard drive, might be a fast SSD, might also be a slow HDD, but regardless, your program wants to get there.. how? Well, your program runs from the CPU, so your CPU will instruct the operation system that it wants to read that file, the OS does the aforementioned housekeeping, and gives a handle or something to the CPU… now the CPU will load the first parts of that file into memory, into it's caches to be more precise. From there, the processing can beginn, and as long as the file is bigger than a few hundred bits, at some point it will have run out of data, and needs to request more from the file, and load it into memory again.
So many moving parts having to work together of course makes this very slow, but there are ways we can improve this. First, when the files are huge, you do not want to process them at the same time you read them, you first want to load them into memory if possible, your CPU can optimize that, since when it knows all of it first goes into RAM, it's most likely just tell the OS to do that. Secondly, many languages have systems that allow you to do this efficiently, but you gonna have to look that up for your language, they are usually called something with “byte” and “file”. This also allows you to parallelize the loading and the actual processing of that file, but be careful here if that actually makes things faster, or if the multithread-housekeeping eats that benefit.
Another way to make this more efficient, a favorite of game engines, is to minimize the amount of files, by putting images together into one big image, called an atlas. This way, you only have one file access and then do a bit of processing to get the images out you need. This approach of course only works well if your images don't change often, or get removed often, but if it is applicable it makes your system a lot faster, especially when combined with the previously mentioned loading into memory.
And lastly, you might just wanna avoid file access as much as possible. The before mentioned output caching will most likely work by putting the request result, in this case, the image, into a fast caching database like redis, and databases can just answer a lot faster since they are made for this. And you might also really wanna serve little thumbnails wherever you can, instead of the real image, using a thumbnail database, and only going for the big file when something important is done with it. You can also make life easy with a S3 service, which does the heavy lifting for you, but this often costs money.
Reading all of this you have to also consider what your server looks like. A bare metal server will perform faster than a normal VPS, even with SSD storage, since with a virtual server, the CPU cores running your program and the actual physical location of your data might be at two different ends of the data center, which makes it easier for your host but harder for you, which is why bare metal hosting is so expensive.

In the end I can just say, don't overdo it. Of course, if your app revolves around a vast amount of images being handled, then you wanna invest as much optimization into it as you can, but if not, don't waste your time setting up complex S3 clusters, just store it in a file, set sensible caching headers, and maybe add an api with image processing and output caching, because most frameworks allow you to easily configure that and it will give you the most benefit for the least amount of work. After all, that's what Wave does, and since I am the best developer there is, you should follow my example /joke.

About the Author

Mia Rose Winter

Software Developer / Project Manager. Full-time cat Woman and bisexual menace. Really not liking tech these days, I have more fun writing stories and books. Developer of GeeksList, Just Short It and Wave.

This might also interest you

A Mystery Involving Hardware Security Modules and Value Tokens

Forbidden Tempura 10/7/2025

Context Historical context In July, 2021, the phenomenon known as the “Gigaleak” continued. The Gigaleak was a drip-feed of part of the ill-gotten data from the 2018 Nintendo data breach. On July 20, 2021, the iqcvs.tar.xz file was uploaded to the now-defunct file sharing website anonfiles.com and thereby made available to the public by The Hacker Known as 4chan. This file contains a dump of CVS repositories. The repository sw contains the BroadOn network infrastructure around the middle of the year 2006. This is shortly before the Nintendo Wii launched. The network infrastructure was initially launched alongside the iQue Player, a variant of the Nintendo 64 featuring downloadable games and some anti-piracy measures of questionable quality (non-HTTPS link) intended for the Chinese market, which was and still is notorious for being particularly prone to piracy. It was developed by a company then called BroadOn Communications Corp., a California corporation. The iQue Player u

ITInfodump

A Brief Look at the 3DS Cartridge Protocol

Forbidden Tempura 6/2/2024

About a week ago, there has been a little addition to the 3dbrew wiki page about 3DS cartridges (carts) that outlines the technical details of how the 3DS cartridge controller and a 3DS cartridge talk to each other. I would like to take this opportunity to also include the 3DS itself in the conversation to illuminate which part of which device performs which step. I will then proceed to outline where I think the corresponding design decisions originate. Finally, I will conclude with some concrete ideas for improvement. But first, we need to talk about parallel universes This protocol makes no sense unless you have a basic overview of the 3DS AES engine. The 3DS AES engine can load 128-bit AES keys in two ways: Using key-derivation from a keyX and keyY (officially called KeyId and KeySeed, respectively). Directly specifying a full AES key. The key derivation from a keyX and keyY works as follows: AES key = (((keyX ROL 2) XOR keyY) + C1) ROR 41, where ROL is left rotation on a 128-bit

ITGamesInfodump

Reconstructing the 3DS Bootstrapping Process at the Factory

Forbidden Tempura 5/13/2024

Motivation The Nintendo 3DS was a fairly popular console. In spite of that, surprisingly little is known about how it is put together at the factory. Working with information that was uncovered during the so-called Gigaleak, I will try to recover as much information as I can about the manufacturing process up and until the point the 3DS is able to complete a normal boot sequence. One-Time Programmable (OTP) region Every 3DS ships with 0x100 of one-time programmable persistent memory at 0x10012000-0x10012100, containing console-unique keys and information. This obviously has to occur before any normal firmware runs on the system because significant amounts of all data written would fail to account for console-unique information and thus the encrypted values would be all encrypted for the wrong keys. An interesting observations: ctr.7z (SHA-256: 8b05072361254437277576d53c08b95e5f076c6b33a2871fad74eaa5561d1d38) ctr/sources/bootrom/CTR/private/build/bootrom/ctr_bootrom/ARM9/main.c has a pr

ITGamesInfodump
Powered by Wave