Client-side engineering lessons from optimizing billions of image loads every day in Meesho’s Android app.
Within the Meesho app, images serve as the product surface itself rather than mere ornamentation. Users judge fabric, finish, colour, fit and detail through pixels, while the application must deliver those pixels across a wide range of Android devices, displays, memory classes and network conditions.
That makes image optimization an unusual engineering problem. A smaller response can improve transfer time and still damage the user experience. A larger response can improve product evaluation and still waste bandwidth, decode time and memory. A request can be fast on the wire but delayed in a dispatcher queue. A response can be cached and still arrive too late to be useful because request preparation or rendering dominates.
The question therefore became: How do we deliver only pixels a surface can use, without losing the detail a shopper needs?
This distinction shaped every experiment that followed.
Image delivery as a system, not a single request
At marketplace scale, an image request consumes more than network bytes. It occupies a dispatcher slot, participates in connection setup and transport, creates CDN and disk-cache identities, invokes a decoder, allocates bitmap memory and finally competes for rendering time on the device.
Consider a square product card rendered at 256 × 256 pixels, which can display 65,536 pixels, while a 1024 × 1024 source represents 1,048,576 pixels, or 16 times the display requirement. Modern compressed formats can greatly reduce transferred bytes without changing the source dimensions, and although the client may downsample during decoding, an oversized source can still add transfer, decoding, scaling and cache work, with the exact cost depending on the image content, format, encoding quality, decoder behaviour and bitmap configuration.
An undersized image creates the opposite failure. A 128 × 128 bitmap stretched to fill the same card may reduce transfer and decode work, but the missing fabric, texture and edge detail cannot be reconstructed on the client.
The useful mental model is a constraint system:
- Visual information: enough resolution and format quality to support product evaluation.
- Time: the first decision- useful image must arrive while the surface still matters.
- Work: network, cache, CPU and memory cost should be proportional to what is displayed.
Optimising one constraint in isolation can make the total system worse. This is why “bytes reduced” or “request latency improved” could never be the final success criterion.

Making the pipeline observable without drowning in events
Our initial measurement problem was that “image load time” collapsed several different clocks into one number. That number could tell us that a user waited, but not why.
The harder problem was volume. A feed or catalogue scroll can trigger many memory, disk and remote loads. Emitting an analytics request for every image would turn observability into its own bandwidth, battery and storage regression.
Instrumentation had to explain each image load without becoming a performance problem of its own. That meant capturing enough screen, image, network and device context to interpret the timings, while avoiding a separate analytics request for every image and preventing stored records from growing without bound on image-heavy screens.
We therefore stored compact per-image records locally, correlated network activity with the eventual Glide result, capped the number of samples collected, and uploaded the records together in consolidated events.
We decomposed the entire pipeline into three user-relevant phases:
- Preparation
- Time from initiating the image load until the network call begins.
- Includes request construction and other client-side setup work.
- Fetch
- For remote loads, measures the network-call lifecycle, including dispatcher wait time, cache processing, and transport.
- Memory and disk hits are classified as separate data sources and are not assigned synthetic network timings.
- Render
- An estimated residual calculated as total load time − preparation time − fetch time.
- Approximates decoding, transformation and delivery to the target; it is a derived residual, not a directly measured rendering duration.
The granular image instrumentation record carries the context required to interpret those clocks: screen, component type, data source, response size, format, CDN metadata, connection type, connection-quality signals, device RAM and CPU characteristics. URL-normalised correlation joins network events to the eventual Glide completion even when eligible format, domain or query transformations changed the URL between initiation and fetch.
Those design choices separated capture frequency from upload frequency. Config controls which screens, component types and data sources are enabled, so a discovery run can focus on PDP hero images, catalogue cards or review thumbnails without enabling the entire image surface.

This instrumentation changed the engineering conversation. Instead of asking, “Which stage looks slow?”, we could ask, “Which specific piece of work is creating the delay, and can it be reduced or removed without weakening the image the user sees?”
Where image-delivery policy lives
Image-delivery decisions were previously spread across feature code, making behaviour inconsistent and app-wide experiments difficult to apply safely.
We moved these decisions into a shared image-loading layer. Product surfaces still choose what to display; once the final display size is known, the shared layer decides how the image should be delivered.
Glide manages request lifecycle, caching, decoding and delivery. A dedicated OkHttp client provides the dispatch boundary, while Cronet handles remote transport to the image service and CDN over QUIC.
The shared layer is the first point that knows both the requested image and the pixels the Android surface can use. That makes it the correct place for a consistent delivery policy.

How networking shapes image load time
Networking affects image delivery before the response body is read and continues until the last byte reaches Glide, so the fetch phase should be separated into request admission, transport setup, server response and payload transfer instead of being treated as one network duration.
- Layer ownership: OkHttp can pool and reuse connections when it owns the socket, but in this image path Cronet owns the remote transport and reports socket reuse, negotiated protocol and connection timings, while the dedicated OkHttp client controls admission through its dispatcher.
- Dispatcher wait: An image call can remain in the OkHttp ready queue when the global or per-host request limit is reached, which makes this interval client-side scheduling delay rather than network connection time.
- Cold and reused transport paths: A reused Cronet connection avoids new DNS, connect and handshake work, while a cold path may perform hostname resolution and establish TCP with TLS or QUIC before request bytes can flow.
- Multiplexing and contention: HTTP/2 and HTTP/3 allow several image streams to share a connection, which reduces repeated setup but does not create additional bandwidth because the streams still share congestion control, throughput and radio capacity.
- Time to first byte: The interval between request send and the first response byte includes network round trips together with CDN cache lookup and any image-service work needed to locate or generate the requested variant.
- Payload transfer: After the first byte, completion time depends on compressed response size, available throughput, packet loss and congestion, which is why response bytes must be interpreted together with protocol and network-quality signals.
- Client hand-off: Cronet request completion marks the end of network transfer rather than the end of image loading because Glide may still write cache data, decode or downsample the resource, apply transformations and deliver it to the target.
The useful network record therefore combines OkHttp queue duration with Cronet socket reuse, negotiated protocol, DNS, connect and handshake timing when those stages occur, time to first byte, received byte count, transfer duration and CDN cache status, allowing a slow fetch to be attributed to admission, cold connection setup, edge or backend response time, or payload delivery.
With these boundaries in place, an experiment can target one measurable cost and be evaluated against the complete image load without assuming that connection reuse, multiplexing or a smaller payload will automatically improve the pixels the user sees.
The experiments changed different parts of the equation
We explored several plausible approaches. They were not wasted work when they failed to become the main business lever; each isolated a different bottleneck or assumption.

More request concurrency.
That question came directly from the queueing behaviour the new instrumentation had surfaced.
Hypothesis: Dispatcher queueing was delaying the first screen when many image requests became ready together in bad network condition. Because images use a dedicated OkHttpClient, we could raise its per-host request limit without changing the application's general API traffic.
Observed result: Queue wait dropped in the targeted condition in the order of seconds, but total delivery did not improve proportionally. Active requests still shared the same connection, bandwidth, decode executors and memory budget. Higher concurrency remained a bounded scheduling control, not the primary optimisation.
Bandwidth-aware downscaling.
Concurrency ruled out scheduling as the sole root cause: active requests were still competing for the same bandwidth and decode capacity.
Hypothesis: On a bad connection, requesting fewer pixels should reduce transfer time enough to improve the product experience. Eligible components therefore moved down one canonical width bucket for a moderate classification and two for a poor classification.
Observed result: Payload size fell by 50% for poor network users, but the connection classification was not stable enough to control image quality. More importantly, the lower-resolution variants removed detail users relied on to evaluate products, so the experiment was rolled back.
Low-quality image placeholders (LQIP).
Bandwidth-aware downscaling showed that removing pixels wasn't safe to do blindly. That raised a different question: could we buy time without touching final image quality at all, by showing something lower-fidelity only until the real image was ready?
Hypothesis: Replacing blank time with a low-resolution image should improve perceived latency. Glide's thumbnail mechanism loaded the smallest canonical width image variant before the final resource.
Observed result: The first pixel arrived earlier approximately 20-30% faster, but the placeholder introduced another Glide load, cache identity and usually another network request that competed with the final image. LQIP remained a scoped presentation technique rather than a default transport optimisation.
Multi-resolution cache lookup.
Placeholders added a second request instead of avoiding one. That pointed at a more basic question: were we even reusing images we'd already downloaded, when we didn't strictly need a new one?
Hypothesis: Since we load different resolutions of the same image for different real estates as explained in later sections, larger cached images should satisfy a smaller target without another network request. The request URL with target viewport width remained the primary source key, the original URL and every higher canonical-width URL became alternate keys.
For a 256-pixel card, a cached 512-pixel variant may be reused and downsampled. The search deliberately excludes 128 and 64 pixel variants because satisfying a larger target by upscaling a smaller bitmap would trade cache hits for lost detail. The small canonical bucket set also keeps alternate-key lookup bounded.

Observed result: Compatible cached work could be reused, but the approach could not improve the first cold request. Reuse remained one-way and opportunistic: a larger cached image may satisfy a smaller target, but a smaller image is never upscaled to manufacture a cache hit.
Async warmup during response deserialisation.
Cache reuse only helped once compatible work already existed, it couldn't create that work sooner. So the last hypothesis went further upstream: could we start fetching images before the screen that needed them was even built?
Hypothesis: If the client discovered image URLs while an API response was still being deserialised, it could start fetching images for the next screen before that screen was built.
The experiment worked as a side path beside normal response handling:
- Response bytes continued to the real JSON deserializer.
- Each consumed chunk was also copied into a bounded 8 KB Okio pipe.
- A background parser scanned that copy for image URLs.
- Up to five remotely configurable URLs were sent to two Glide prefetch workers.
A prefetch helped only when the selected image later became visible and the UI generated the same Glide cache key. Otherwise, the speculative request consumed bandwidth and worker capacity without improving the screen.

Observed result: Though prefetching accelerated the future images by 50-80 ms when the predicted URL and eventual Glide cache key matched. The speculative work started competing with the critical path in terms of CPU and Memory.Hence, It was not adopted as a general default.
The breakthrough was useful pixels on the measured surface.
The highest-impact result came from a simple rule applied throughout the app: use the width of the rendered surface to request an appropriately sized image from the backend.
Once the UI knows the final display width, the shared image layer rounds it up to the nearest supported size. The backend then returns that image variant. For example, a 430-pixel surface requests a 512-pixel image rather than a smaller image that would need to be stretched.
Centralising this decision gives every screen and real-estate the same behaviour. Small surfaces avoid downloading far more pixels than they can display, while larger surfaces still receive enough detail.

This policy produced the strongest outcome because it eliminated unnecessary work without asking users to accept less product detail. It improved more than transfer time: fewer unnecessary pixels meant smaller responses, lower decode cost and reduced bitmap memory pressure. It helped improve image quality of some real estates by even 100% on high end devices and also helped reduce image size by 50% on some real estates.
The rule sounds simple, but applying it safely required the rest of the system:
- The UI had to expose the final display width.
- The shared layer had to apply the rule consistently across features.
- Supported width buckets had to avoid upscaling.
- URL and cache identities had to remain predictable.
- The backend and CDN had to generate and reuse the same variants.
- Instrumentation had to validate performance and product impact.
Without these pieces, width-based resizing could fragment the cache, request an undersized image or optimize one surface while leaving the rest of the application unchanged.
What we learned
The experiments produced principles that now guide image-delivery work across the application:
- Reduce work before scheduling it: Concurrency can change when work begins, but it cannot create bandwidth, decode capacity or memory.
- Do not let an unstable signal own quality loss: Network conditions change quickly; missing image detail cannot be recovered after an undersized variant is selected.
- An earlier pixel is not necessarily a useful pixel: A placeholder helps only when it reduces meaningful blank time without delaying the final decision-useful image.
- Reuse must preserve information: A larger cached image can safely serve a smaller surface. The reverse direction trades a cache hit for lost detail.
- Speculative work must be isolated: Prefetching is safe only when backpressure cannot delay the response needed to construct the screen.
- The best platform rule is measurable and difficult to bypass: Centralising policy allows one decision to be applied, observed and corrected across every eligible surface.
The decisions that survived measurement became the architecture: shared observability, a single image-policy layer, canonical width selection and one-way cache reuse. The rejected approaches were still valuable because they retired assumptions that might otherwise have returned.

Where we go next
The next opportunities sit at the boundary between networking and image processing. Better compression can reduce transfer work only when format support and device-side decode cost are validated end to end. CDN efficiency depends on predictable variants and cache identities. Runtime network signals may guide scheduling or prefetching, but should not reduce product quality unless their accuracy is proven.
Each future change will be evaluated against the same three constraints: visual information, time and total system work.
At Meesho’s scale, the largest wins do not necessarily come from the most elaborate mechanism. They come from small, enforceable rules that remove unnecessary work from billions of repeated operations.
Building the visibility, architecture, and evidence to prove that rounding a width to the next supported bucket was the correct decision for the entire image-delivery system was more challenging than writing the function itself.
The right pixels are the fewest pixels that preserve the shopper’s decision, delivered with the least total work.

