Meta: Debugging slow images? Use this guide to trace storage delays, cache misses, and heavy transformations.
When users complain that a page "feels slow," the first thing engineers usually check is images. It’s easy to assume that the fix is simply using the appropriate format, such as WebP over JPEG or AVIF over WebP. But real-world performance issues are rarely that simple.
More often than not, the actual issue isn't even the format. The slowdown could be hiding in:
- A proxy layer resizing images in real time
- A CDN that's not caching as expected
- Storage buckets with slow cold fetches
- Oversized payloads due to missing responsive settings
All of these can add milliseconds (or even seconds) to load times and, in aggregate, create frustrating, hard-to-reproduce regressions.
This article walks you through how to identify bottlenecks using hands-on tools, from browser DevTools and CDN headers to distributed tracing tools such as OpenTelemetry. And sometimes the slowness might not come from the network at all, but from how the image is served or resized. Along the way, we’ll also explore how introducing an image resizer into your workflow can help deliver images more efficiently without adding unnecessary latency.
What “slow” actually means
When a user says a page is "slow," that complaint can refer to several things depending on where you're measuring it.
From a performance perspective, image speed is generally tied to three moments:
- How quickly the browser starts receiving data from the server (Time to First Byte, or TTFB)
- How quickly the dominant image finishes rendering (Largest Contentful Paint, or LCP)
- Whether anything concerning the visual feels jarring or delayed
For example, a product image on an e-commerce site might be visually appealing in terms of format and served from a CDN, yet it could still be slow if the CDN retrieves it from cold storage. In another case, the same image might be pushed out to the browser very quickly, but heavy on-the-fly resizing or reformatting in a proxy layer can push LCP into unacceptable ranges.
These sorts of delays are cumulative. A few hundred milliseconds spent on a cache miss, another chunk on storage retrieval, and a bit more on transformation can add up to a noticeable pause. That’s why measuring across the entire delivery chain is better than examining one phase in isolation.
"Slow" is never a single figure. It's a combination of timings across network, storage, proxy, and rendering, and understanding where the slowdown occurs is the first step to fixing it.
Let’s explore how the easily accessible resources provided by your browser and CDN can be utilized to identify what slows down images. These initial layers of inspection tend to disclose the most significant bottlenecks and conserve time during debugging.
Tracing slow images with DevTools and CDN headers
Now that you understand what “slow” means, the next step is to identify exactly where delays occur. The most convenient place to begin this inquiry is with the browser's built-in developer tools.
Open your browser's DevTools and navigate to the Network panel. Filter for images and examine closely:
- Check the TTFB (Time to First Byte) to see if the delay is server-side. If it's high, the delay is likely server-side, either at your storage origin or your proxy service.
- Look at the download time to see the actual time it took to transfer the file. Larger payloads or poor network conditions will be apparent here.
- Check the status codes to see if it is being served from the cache. A 200 (OK) response indicates that the picture was served up fresh, and a 304 (Not Modified) response indicates that a cached copy was returned.
Use the Performance tab to watch the impact on LCP and visual rendering. This helps you correlate network delays with the user experience.
In the Network panel, you can view the duration of the image request. The screenshot above shows an image download that lasted almost two seconds. The breakdown reveals delays in multiple stages, with over 600 ms waiting for the server response and nearly 900 ms for the content download itself. These timings clearly indicate where bottlenecks are occurring. The issue is server-side latency, transfer size, or both.
Beyond the browser, CDN response can also reveal more information. For example, headers like Cache-Control specify how long images are cached, while Age indicates when the cached version was generated. Low age and high cache miss rates typically indicate poor CDN configurations or excessive cache flushing.
If you do have backend access logs, light logging of image requests can be highly informative on where time is being consumed. For instance, logging request timestamps, cache hits, or the length of resizing processes helps highlight bottlenecks in your proxy or storage layers.
By combining these approaches, you form a complete picture of where slowdowns occur. This visibility helps determine when simple fixes are sufficient or when it’s time to escalate to more advanced tracing tools to resolve complex issues.
When to escalate to distributed tracing
Sometimes, browser DevTools and CDN logs aren’t enough to uncover the root cause of slow images. Complex setups, especially those involving proxies, dynamic resizing services, or upstream storage systems, can hide latency in layers not visible from the frontend alone.
This is where distributed tracing tools like OpenTelemetry are super helpful. They provide end-to-end visibility by tracking requests across services, helping you pinpoint exactly where delays occur, whether in image proxy layers, resizing pipelines, or storage backends.
Why use distributed tracing?
- Trace entire request flows: Follow image requests through multiple services and network hops.
- Identify hidden bottlenecks: Check for latency in proxy layers or resizing processes that aren’t logged elsewhere.
- Correlate events: Connect slowdowns to specific operations or external dependencies.
You should consider distributed tracing when you’ve already examined browser timings, confirmed CDN cache behavior, and reviewed backend logs and storage performance, but still encounter unexplained delays in image loading. At that point, tracing provides deeper insight into the hidden layers causing the slowdown.
The hidden cost of image processing
Once network and caching issues are addressed, the next bottleneck often appears in the transformation layer, where images are resized, compressed, or reformatted before delivery to the browser.
This layer is deceptively tricky:
- A resize operation may appear harmless, but if it is performed on each request instead of being cached, it adds processing time.
- Format conversion, such as JPEG → WebP, can spike CPU load if it's not optimized.
- Very large "one-size-fits-all" originals cause browsers to download extra bytes, especially on mobile devices.
Because these operations sit between your storage and your users, even small inefficiencies add up to noticeable LCP slowdowns.
This is where image processing libraries can be handy. They sit in the transformation layer, handling tasks such as resizing, compression, and format conversion before images reach the browser. To see what this looks like in practice, we’ll use imgproxy, a self-hosted tool specifically for real-time image optimization and delivery.
Resizing without the wait with imgproxy
At its core, imgproxy is a lightweight, open-source image processing server. Instead of shipping raw, heavy images directly to the browser, it transforms them on the fly, resizing, compressing, and even converting them into more efficient formats, such as WebP or AVIF.
Here’s the power behind the tool:
- On-the-fly resizing and cropping
- Automatic format conversion for better compression
- Quality tuning to balance size vs. sharpness
- Cache-friendly URLs so transformed images can live at the CDN edge
In this section, we'll use imgproxy to resize our original image. We will then compare the loading speed of the resized image to the original to demonstrate the performance benefits.
Installing imgproxy is straightforward. For those familiar with Docker, the official documentation provides all the steps to get the service running on your local or remote machine. For a one-click solution, which is what we will use in this section, you can use the "Deploy to Heroku" button found in the documentation to spin up a fully operational instance.
Once you click the button, you will be directed to this screen:
Now, pick a name for your app and take note of the IMGPROXY_KEY and IMGPROXY_SALT values listed below. They’ve already been generated for you. Just copy them somewhere safe. Together, they form a powerful security feature that imgproxy uses to protect your images.
Once you have your imgproxy URL and key/salt pair ready, head over to the imgproxy URL generator. Use the image we’ve already tested for speed. Keep in mind that, by default, imgproxy won’t allow images with dimensions larger than 4096 pixels on either side.
Paste the image URL, along with your imgproxy host URL, key, and salt, into the appropriate fields. Choose your desired dimensions and crop mode, then hit Generate. Finally, copy the URL that appears and open it in a new browser window.
Let’s open DevTools again to see how fast imgproxy delivers the resized image. This time, the request completes almost instantly.
With imgproxy, the resized image is delivered in just 3 µs, compared to nearly 2 seconds for the unoptimized version. That’s faster, with the total request duration dropping from around 1.9 seconds to only 22 µs. (For context, 1 µs is one-millionth of a second). Read this article to gain a deeper understanding of the waterfall explanation.
Here’s how they compare:
| Metric | Raw Image (unoptimized) | imgproxy Resized Image | Improvement |
|---|---|---|---|
| Content Download | ~2 seconds | 3 µs | ~600,000× faster |
| Total Request Time | ~1.9–2.0 seconds | 22 µs | ~90,000× faster |
| Impact on Rendering | Sluggish LCP, heavy load | Much faster LCP | Noticeably smoother |
Wrapping up
Slow images aren't always about the format. They're a function of the journey those images take from disk to screen. Every millisecond spent on cache misses, cold loads, and heavyweight conversions adds friction to your users' experience. Visibility is the answer: knowing exactly where those milliseconds go and addressing them at the right layer.
By combining the browser DevTools and CDN insights with the increased visibility of distributed tracing, you can identify and fix bottlenecks before they impact conversions or engagement.
If you find the bottleneck is in the transformation layer, that is exactly where a tool such as imgproxy can shine. It will not replace higher-level tracing and observability, but it can ensure that resizing and format conversion don’t become hidden slowdowns in your pipeline.





Top comments (0)