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.
