How do image-loading libraries like Glide/Coil manage memory vs disk caching, and how would you configure one for a feed-heavy app?
Glide and Coil both juggle a bounded in-memory LRU, an on-disk cache, and aggressive downsampling — get any of the three wrong and an infinite-scroll feed turns into an OutOfMemoryError.
LEARN BY DOINGPut your intuition to work3 steps
How do image-loading libraries like Glide/Coil manage memory vs disk caching, and how would you configure one for a feed-heavy app?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Two caches, two different jobs
Start with why there are two caches at all. A decoded bitmap ready to be drawn on screen is enormous compared to the compressed bytes it came from — a 1080×1080 JPEG that weighs 150 KB on disk decodes into an ARGB_8888 bitmap of roughly 1080 × 1080 × 4 bytes, about 4.6 MB, sitting in RAM. A feed screen showing a dozen visible-plus-buffered images at once is already tens of megabytes of decoded pixels, and Android's per-app heap is measured in the hundreds of megabytes at best. That single fact is why both Glide and Coil split caching into two tiers with very different trade-offs: a small, fast, volatile memory cache holding ready-to-draw bitmaps, and a larger, slower, persistent disk cache holding something closer to the original bytes.
#The memory cache: bounded, fast, first to go
The memory cache is an LRU (least-recently-used) keyed by request signature — URL plus any transformations, target size, and format — mapping to a decoded Bitmap. Both libraries size it as a fraction of the app's available memory rather than a fixed number: Glide's MemorySizeCalculator looks at ActivityManager.getMemoryClass() and screen resolution to pick a sane default (roughly 1/8th to 1/4th of available memory, tunable), and Coil's MemoryCache.Builder exposes maxSizePercent() for the same idea. Because it's an LRU, scrolling a feed continuously evicts the images that scrolled furthest off-screen to make room for new ones — that's expected and fine, it's a cache, not storage. What's worth remembering for an interview: this cache is wiped whenever the process dies, and both libraries hook ComponentCallbacks2.onTrimMemory() to proactively shrink or clear it under system memory pressure (background app, TRIM_MEMORY_UI_HIDDEN, TRIM_MEMORY_COMPLETE), which is exactly the right behavior — better to re-decode from disk than to be the app the low-memory killer picks off first.
Glide adds a second in-memory structure most people forget about: the BitmapPool. Instead of only caching finished bitmaps, Glide keeps a pool of "dead" Bitmap byte arrays it can hand back to BitmapFactory via inBitmap, letting the decoder reuse the same backing memory for a same-sized, same-config decode instead of allocating fresh and generating garbage. It only helps when dimensions and config line up almost exactly, which is why feed grids that mix wildly different aspect ratios get far less benefit from pooling than a feed of uniform thumbnails.
#The disk cache: what actually gets written matters
The disk cache is where the two libraries diverge most, and it's a favorite follow-up question. Glide exposes DiskCacheStrategy per request: DATA caches the original, untransformed source bytes (cheap to write, but every load still has to redo centerCrop/resize/decode); RESOURCE caches the final, transformed bitmap re-encoded to disk (more disk space per image, but a cache hit skips transformation entirely — the default choice for remote images that get expensive transforms); ALL caches both; AUTOMATIC, the default, picks RESOURCE for remote data and DATA for local data, on the theory that re-fetching a local file is nearly free but re-fetching a remote one isn't. Coil takes a simpler, single-tier approach: its DiskCache stores the raw fetched bytes (what came back from the network or Fetcher), never the post-transformation pixels — a disk-cache hit still re-applies your Transformations and re-decodes at the target size every time, trading a bit of CPU for a much simpler cache-invalidation story.
#Downsampling is the lever that actually prevents the OOM
Neither cache tier matters if you decode at full resolution. Both libraries use BitmapFactory.Options.inSampleSize (or the modern ImageDecoder equivalent) to decode directly at a size close to what will actually be drawn, which is why giving them an explicit target size is the single highest-leverage thing you can do in a feed: Glide's override(width, height), Coil's size() (or its default ViewSizeResolver, which waits for the target ImageView to be laid out and reads its measured dimensions). Skip this — say, by loading into a wrap_content ImageView with no explicit size — and you risk decoding a 4000×3000 camera photo into memory purely to shrink it down for a 300 dp thumbnail, which is the classic feed-screen OOM.
#Configuring it for a feed
A feed is a worst case for image caching: an effectively unbounded, ever-scrolling stream of distinct images, mostly seen once and never revisited in the same session. That reshapes the defaults you'd reach for on a smaller screen. Size the disk cache generously — disk is cheap, and a warm disk cache means a user re-opening the app doesn't re-download last session's feed — but keep the memory cache modest, since it's shared with everything else the process is doing and a feed's images are inherently more disposable than, say, a profile avatar shown on every screen. Fix your target decode size to the exact pixel dimensions of the grid cell (not the maximum the layout could grow to) so every request for the same cell converges on the same bitmap-pool bucket. Preload a screen or two ahead of the visible viewport using RecyclerViewPreloader (Glide) so decoding happens during idle scroll frames instead of janking the frame the image becomes visible; Coil achieves the same effect by prefetching URLs through ImageLoader.enqueue() ahead of scroll position. And skip the memory cache for anything shown exactly once and unlikely to be revisited (a full-bleed image opened from a detail screen) with skipMemoryCache(true) — otherwise a handful of large one-off views can evict the small thumbnails a user is more likely to scroll back to.
// Glide: a single Application-wide module configuring both cache tiers.@GlideModuleclass FeedGlideModule : AppGlideModule() {override fun applyOptions(context: Context, builder: GlideBuilder) {val calculator = MemorySizeCalculator.Builder(context).setMemoryCacheScreens(2f) // ~2 screens worth of decoded bitmaps in RAM.build()builder.setMemoryCache(LruResourceCache(calculator.memoryCacheSize.toLong())).setBitmapPool(LruBitmapPool(calculator.bitmapPoolSize.toLong())).setDiskCache(InternalCacheDiskCacheFactory(context, "feed_image_cache", 250L * 1024 * 1024)).setDefaultRequestOptions(RequestOptions().diskCacheStrategy(DiskCacheStrategy.AUTOMATIC))}override fun isManifestParsingEnabled(): Boolean = false}// Per-item request inside the feed adapter's onBindViewHolder.Glide.with(imageView).load(post.thumbnailUrl).override(feedCellWidthPx, feedCellHeightPx) // forces downsampling to the exact cell size.centerCrop().placeholder(R.drawable.placeholder_shimmer).into(imageView)// Preload a couple of screens ahead while the user is still scrolling.val preloader = RecyclerViewPreloader(Glide.with(this),feedPreloadModelProvider,ViewPreloadSizeProvider(),/* maxPreload = */ 12,)recyclerView.addOnScrollListener(preloader)
// Coil: one ImageLoader shared app-wide, sized explicitly for a feed.val feedImageLoader = ImageLoader.Builder(context).memoryCache {MemoryCache.Builder(context).maxSizePercent(0.20) // 20% of available app memory.build()}.diskCache {DiskCache.Builder().directory(context.cacheDir.resolve("coil_feed_cache")).maxSizeBytes(250L * 1024 * 1024).build()}.bitmapConfig(Bitmap.Config.HARDWARE) // display-only bitmaps, barely touches the JVM heap.build()Coil.setImageLoader(feedImageLoader)// Per-item request; size() forces downsampling to the exact cell size.imageView.load(post.thumbnailUrl) {size(feedCellWidthPx, feedCellHeightPx)scale(Scale.FILL)crossfade(150)placeholder(R.drawable.placeholder_shimmer)if (isOneShotDetailImage) memoryCachePolicy(CachePolicy.WRITE_ONLY)}
🧠 Image caching check
0/3 · 0/3 answered1. A feed shows a 4000x3000 photo inside a 300dp-square grid cell, loaded with no override()/size() call. What's the most likely consequence?
2. Why might Glide's DiskCacheStrategy.RESOURCE outperform DATA for a remote feed image with a heavy transformation like a rounded-corner crop?
3. Why does Coil re-apply transformations on every load even when the disk cache hits?