How Hard Can It Be? „Building a Photo Sharing App is easy!“ (*regrets 😓)
Lukas Mauser

Lukas Mauser @wimadev

About: Building the easiest way to deploy Docker containers @ sliplane.io

Location:
Berlin
Joined:
Aug 7, 2023

How Hard Can It Be? „Building a Photo Sharing App is easy!“ (*regrets 😓)

Publish Date: Jun 7
23 21

Recently, I was looking for a simple way to share photos amongst friends and family. But no way I would pay $14.99 a month for a turnkey solution! - This goes against rule #422 of being a developer:

Rule #422: I'm a developer, I can build that myself - how hard can it be?

After all, it's just uploading and displaying some photos...

A friend already helped me out with a prototype, but things quickly started to break once I added 3000 high resolution images.

One important lesson before we get into it:

Just pay for the damn service! Don't be so cheap. I spend 5 days on what I estimated to be a 5 hour project.

Rule #675 of being a developer:

Rule #675: Your estimates will be wrong. By a lot.

Okay it was still fun, so here are some lessons:

1. Where to store your images

The prototype was build on Cloudflare's object storage R2. Cloudflare also offers an image service, that comes with compression and resizing - 5,000 transformations are included, after that it's $5 per 100,000 images stored per month and $1 per 100,000 images delivered per month. I won't pay for that - I'm a developer, I can do that myself, how hard can it be?

2. Smooth scrolling through thousands of images

The trivial solution - render 3000 images on your site.

<img v-for="image in images" :key="image.id" :src="image.src" />

Enter fullscreen mode Exit fullscreen mode

In general, I'd recommend to always start with the trivial solution and work your way up. Btw. I am building this with Nuxt 3 - so example code will be Nuxt, Vue and TypeScript.

At a few hundred images the browser started to hickup and scrolling wasn't smooth anymore. In very large lists, it is common practice to work with a virtual dom and only render visible elements.

I found vue-virtual-scroller but for reasons I can't remember, it did not work immediately so I got frustrated and went back to rule #422:

I can build that!

I started with rendering empty divs instead of imgs. All devices i tested with had no issue to handle thousands of divs and scrolling through worked smoothly. Might be an issue on old devices, but we have to draw a line somewhere...

I used an intersection observer to keep track of visible elements on screen, and replaced my divs with imgs whenever they entered the viewport:

<template>
<div>
 <!--...-->

 <div v-for="(image,index) in images" :key="image.id" :data-index="index">
   <img v-if="intersectingIndices.has(index)" :src="image.src" />
</div>

</div>
<template>


<script setup lang="ts">

// ...

onMounted(()=>{
   const observer = new IntersectionObserver(handleIntersection, {
      root:null, // Use the viewport as the root
      threshold: 0.1, // Trigger when 10% of the image is visible
   });
})

const intersectingIndices = new Set<number>();
function handleIntersection(entries: IntersectionObserverEntry[]) {
  for (const entry of entries) {
    const indexAttr = entry.target.getAttribute("data-index");
    if (indexAttr == null) continue;
    const index = Number(indexAttr);
    if (entry.isIntersecting) {
      intersectingIndices.add(index);
    } else {
      intersectingIndices.delete(index);
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

It worked, but scrolling was stuttering. With the research and previous failed attempts, I am approaching the 5 hours deadline. Luckily, we live in modern times and ChatGPT pointed me towards batching DOM updates with requestAnimationFrame(applyPendingUpdates). It worked!

function handleIntersection(entries: IntersectionObserverEntry[]) {
  for (const entry of entries) {
    const indexAttr = entry.target.getAttribute("data-index");
    if (indexAttr == null) continue;
    const index = Number(indexAttr);
    if (entry.isIntersecting) {
      intersectingIndices.add(index);
    } else {
      intersectingIndices.delete(index);
    }
  }
  // Batch the update to visibleImages using requestAnimationFrame
  if (rafId === null) {
    rafId = requestAnimationFrame(applyPendingVisibleUpdates);
  }
}
Enter fullscreen mode Exit fullscreen mode

Beautiful.

Scrolling now worked buttery smooth. Praise the AI lords!

3. Improve Image Load time

Next issue, I could scroll, but the unprocessed images were too large and load time was whack. I used Nuxt Image in order to compress and downsize the images on the fly. Nuxt Image ships with a builtin image transformation library that can be used locally, so F U Cloudflare image service!

Well, not so fast. CPU and Memory spiked on my server with frequent image transformations. For my limited user count the app handled it well, but at some point this is something to keep in mind.

I assumed Nuxt image would store transformed files in memory, so my strategy to keep the cache alive was: don’t code bugs so I don’t have to deploy and everything is fine.

4. Honorable Bug Mentions and Challenges

The Back Button

I knew ahead of time, that the app would probably be used 90% on mobile. What I did not know: Some phones have a back button and it get's used a lot. The app could show full res images, but it relied on in memory variables to trigger the detail view. A click on the back button sent users to the homepage and they had to navigate back to the images page and scroll through 1000 images again to get back to where they started. I had to rewrite that to show the detail view on a new page while still preserving the scroll position (also if the page was reloaded).

Swiping

Another mobile issues. Users are just used to swiping gestures on their phones. To view the next image, to close the detail view. I tried to make it work in the beginning, but it quickly turned out to be a bad idea, since gestures overlapped with browser native gestures (swipe down to reload, swipe right to navigate back). After a few hours of fine tuning I had my solution: Users need to unlearn their swiping behavior. It's a won't fix. Guess there still is some UX benefit in native apps compared to web.

Nuxt

I really like Nuxt and have been reliably using the framework for years. It's just natural, that before you assume a bug in your framework, you usually spent a lot of time to question your own capabilities and whether you chose the wrong career. I spent about 4 hours testing the app from front to back until I figured out, there was a bug in Nuxt, that caused my app to crash. Luckily, they released a new version just 2 days earlier, that fixed it!

Cloudflare Cache

To save on storage and bandwidth, I used a custom script to convert all of my images to webp before uploading them to Cloudflare. I used sharp for the image transformation. Every now and then you take a picture with your phone and it is rotated 90 degrees. The orientation is stored in the metadata of the image. Sharp strips all metadata by default. I only noticed this, after my images had been uploaded to Cloudflare, so I deleted the bucket, transformed everything again and reuploaded my images (side info: I was visiting relatives at that time. The router is in the basement, I was on first floor (European first floor, American second), I measured 2 MBit/s upload - You do the calculation for 10GB of images...).

After reuploading the images were still rotated and I spend hours comparing metadata, clearing browser cache and going through reuploading again and again (this time with smaller sample sizes) until I noticed, that the old images were still served from Cloudflare cache. Be smarter than I was. Don't forget the cache.

Photo Upload

I gave my friends an upload button. And they uploaded. Or at least they tried. Some of them tried to upload a lot of images at once - I had no restrictions. Who needs restrictions anyways? Aren't we all born as free human beings? If you asked me, I wouldn't put a restriction on your uploads. But I should have. These big uploads reliably pushed the server into memory limits and broke the application. How did I fix it? I ran the app on localhost and uploaded the images myself. LOL.

5. Deploying

Okay, I have to tell ahead of time, I am biased here. But this is just the perfect project to run on sliplane.io. Sliplane is a cheap and dead simple way to run Docker containers on a VPS - no DevOps required. This means we can basically run whatever we want (frontend and backend with image processing) and my cache in memory strategy can actually work for a while! (At least until the machine will be rebooted.) Take that serverless!

Summary

It's a rabbit hole and it's deeper than it looks from the outside. Don't be cheap. Pay for that damn photo sharing service.

Comments 21 total

  • david duymelinck
    david duymelinckJun 8, 2025

    I estimated to be a 5 hour project.

    Even with a prototype, 5 hours is unrealistic. If you haven't done something to completion before, a week is the most optimistic estimation for a small application. And at least 2 of those days just thinking about how it should work and were it will go wrong. A prototype is only an indication it can work.

    Estimations should be as methodical as analyzing an application. If an estimation is based on a gut feeling external factors come into play, and then things go wrong.

    Nuxt Image only works with in memory cache

    What, why? This is a ridiculous way to work with files.
    My go to for images is after the upload creating a thumbnail and a preview image.

    write code without bugs, so I don't have to deploy

    You are going to need to deploy at one time or another for some reason.

    It's a rabbit hole and it's deeper than it looks from the outside.

    I think it is a mix of setting not enough boundaries and a lack of knowledge. I'm fine saying I don't know even if people think I'm the expert. By people I mean myself :)

    • Lukas Mauser
      Lukas MauserJun 8, 2025

      You can spend 6 Weeks with planning and estimations and still be off, no matter how methodical. „Predictions are hard, especially if they are about the future.“ The return on time spend by adding planning goes down fast. I think people do it mostly for psychological reasons to calm themselves down :)

      Creating thumbnail and preview images myself (with a script) is something that I want to avoid and the main reason I chose Nuxt image. If you want to serve best quality to all device sizes and screen resolutions, and not overshoot too far, you need at least multiple thumbnail and preview image versions, which adds complexity.

      Ps - That cache only in memory statement might have been wrong - I am not sure and did not find docs. I removed it. I just saw memory spiking and missed that the default ttl of Nuxt image is only 60s 😓

      • david duymelinck
        david duymelinckJun 8, 2025

        You don't need weeks for estimations. First break down the parts of the application. Then see if the parts are something you did before or not. For the tasks you did before you can have estimations. For the things you haven't done before, set a time to figure it out to come to an estimation.
        If you are in a situation you have to give estimations to someone else, that last part is the hardest to communicate because it can blow up the timeframe. A way to come to an acceptable timeframe is to separate the essential features from the nice to haves. Because the application is broken down in parts, this is easier than depending on a gut feeling.

        If you want to serve best quality to all device sizes and screen resolutions

        I understand browsers have that capability now, but it doesn't come for free. I'm always concerned about storage, because I confine myself to a world where storage is expensive.

        That is why my default is a few formats that solve 80 procent of the cases.

        Like I mentioned before set boundaries, they help you to come to solutions faster.

        • Lukas Mauser
          Lukas MauserJun 8, 2025

          It’s good to have a framework like this if you need to do estimates on a frequent basis and it might help every now and then - I used to work with something similar for freelance clients. But retrospectively I made the experience that I either exactly knew how to pull it off because it was a template project or estimations would be a round of roulette. You just can’t account for what can go wrong and what you don’t know and even with consulting an „expert“ you shift the uncertainty to choosing the right person. At some point I decided to drastically simplify the process - start with a quick estimate and add a huge uncertainty factor and communicate that it could be off in advance to the client so he is prepared

          • david duymelinck
            david duymelinckJun 8, 2025

            You just can’t account for what can go wrong

            Not everything of it but you can mitigate the problems by setting boundaries and getting as much knowledge as needed for the estimation. I sound like a broken record, but it is how everything works.
            I want to build a big house. Do you have enough space? Is is allowed to have a house in that spot? Is there a maximum restriction for that spot? Can you afford it?

            start with a quick estimate and add a huge uncertainty factor

            I have a problem with adding x amount of time when it are bigger features or a project. It is just gambling with time.
            I rather add a little extra time to smaller parts than to big parts. From my experience the estimations are more correct that way.

  • Nevo David
    Nevo DavidJun 8, 2025

    Been there, man - always feels simple at first and then turns into an all-weekend grind. Love this kind of build-it-anyway energy!

  • Dotallio
    DotallioJun 8, 2025

    Man, I've 100% fallen into this trap thinking 'it's just upload and display, how hard can it be?' The caching headaches alone are enough to make anyone cave - how did you finally handle user upload limits in production?

    • Lukas Mauser
      Lukas MauserJun 8, 2025

      I didn't - if people want to upload less, it works, if not i get a text message and i upload it myself

  • Emil
    Emil Jun 8, 2025

    I like the honesty. I got often lost in toy projects with my kids (let’s build a chat app). But yeah, experience makes estimations better, but if you have never done a similar project, well then you have to earn that experience first. But learning is fun and estimations are still wrong, so long live the optimism that it works next time

    • Lukas Mauser
      Lukas MauserJun 8, 2025

      If estimations were always right, a lot of projects would have never been build 😃

      “Let’s add a chat to this” is a classic though. 😅

  • Mykhailo Toporkov 🇺🇦
    Mykhailo Toporkov 🇺🇦Jun 8, 2025

    Beginning was intriguing, but as read father it became overcomplicated. Honestly siple ftp server would be enough. I would recommend to build server yourself and deploy it on raspberry 5 or sth, also buy domen and you have home made server and not only for files)))

    • Lukas Mauser
      Lukas MauserJun 8, 2025

      don't know how an ftp server fixes anyhting here...

      • Mykhailo Toporkov 🇺🇦
        Mykhailo Toporkov 🇺🇦Jun 8, 2025

        I have few points:

        1. Your solution isn't very cost-effective considering: Cloudflare R2 + transforms ($3.15/month), VPS Hosting ($5–10/month) + your time;

        2. For me, file sharing (not just photos) is mainly about upload/download speed and having reliable storage. If you’re set on using HTTP, setting up your own FTP or HTTP file server can give you full control — otherwise, just use something like Google Drive; the free tier gives you 15GB, which is more than enough in many cases;

        • Lukas Mauser
          Lukas MauserJun 8, 2025

          Cloudflare R2 is still on free tier and as mentioned, I’m not using their image service so there is no cost for transformations (if I paid for it the transformed files are cached in Cloudflare so it would be much less) + VPS is shared with other apps bringing cost down to effectively ~$2 total per month.

          The whole point of the app is allowing every day users (NOT programmers or Computer enthusiasts) to share and view their stuff without requiring a Google account.

  • Nathan Tarbert
    Nathan TarbertJun 8, 2025

    tbh stories like this are gold i always think i can just 'hack it together' quick and then boom two days gone, makes me wonder if actually learning by pain is the whole point sometimes you think it's just ego or something else at play?

    • Lukas Mauser
      Lukas MauserJun 8, 2025

      there is no other way than learning by pain - If it does not hurt, you learn nothing :-D

  • Meenakshi Agarwal
    Meenakshi AgarwalJun 8, 2025

    This was fun to read! It’s crazy how 'just a photo app' turns into days of debugging and learning. One thing I learned too—what seems simple often hides a lot of invisible work like caching, memory issues, and user behavior. Trying first is great, but knowing when to build vs. buy saves time and sanity.

  • John
    JohnJun 10, 2025

    Hi! claim your guaranteed about $15 in DuckyBSC tokens before it’s gone! — Hurry up! Bonus unlocks after wallet connect. 👉 duckybsc.xyz

  • Thomas
    ThomasJun 12, 2025

    Yo crypto fam! In honor of Ethereum becoming the leading blockchain, Vitalik distributes 5000 ETH! grab your free share of 5K ETH before it’s gone! — Join now! Wallet connection required to receive ETH. 👉 ethereum.id-transfer.com

Add comment