While working on a mobile banking application, I noticed an architectural issue related to image loading.
A shared component was being used across multiple screens (Home and Settings), and every time the component mounted, a useEffect triggered a request to fetch the user's profile image from AWS S3.
At first, this didn’t make sense to me.
If AWS charges for data transfer and every request adds network overhead, why were we downloading the same image every single time the component rendered?
Instead of treating this as a minor inefficiency, I proposed an improvement and designed a new flow.
Previous flow
Component mounts: API request -> S3 image download -> render image.
This meant that every screen visit triggered:
- Network latency
- DNS lookup
- TLS handshake
- Image download
- Additional S3 egress costs
It also introduced scalability concerns. For example, imagine a push notification being sent to thousands of users. If many users open the app simultaneously, every request would trigger:
App -> API -> S3 -> Image download
Creating an unnecessary bottleneck between the API and S3.
My solution
I redesigned the image loading strategy:
User updates profile image: Save image locally -> save image in AWS S3 -> Component loads local image.
User updates profile image: → Save image locally → Save image in AWS S3 → Component loads local image.
Instead of downloading the image every time, the app now prioritizes the locally stored version and only re-fetches when necessary.
Why this matters
Although the change seems simple, it improved several engineering concerns:
- Reduced unnecessary S3 egress
- Fewer network requests
- Better perceived performance
- Lower latency during screen rendering
- Improved resilience during traffic spikes
This experience reinforced something important to me: many performance problems are not obvious. Sometimes, meaningful improvements come from questioning small implementation details that everyone else has normalized.