A Flutter real-estate gallery of large remote photos crashes with out-of-memory on older phones; how does ImageCache behave, and what would you change?
answer
- 1000 entries, 100 MiB by default
- live images are not counted
- bigger than the byte limit: not cached
- memory pressure clears the cache
- decode smaller before tuning limits
basics
~20 sImageCache keeps up to 1000 decoded images or 100 MiB, but images still on screen are live and not counted. Such crashes usually come from decoding full-resolution photos, so decode smaller with cacheWidth first, then tune the limits.
solid answer
~40 s`PaintingBinding.instance.imageCache` has three parts: pending loads, an LRU cache of completed images capped by `maximumSize` (1000) and `maximumSizeBytes` (100 MiB), and **live** images, those whose stream still has listeners, such as every photo on screen. The byte limit governs only the LRU part, so twelve full-resolution photos visible at about 46 MiB each blow past any limit; an image bigger than `maximumSizeBytes` is not cached at all. On a memory-pressure signal Flutter calls `imageCache.clear()`, which also leaves live images alone. So I fix decode size first with `cacheWidth` per context, keep the gallery lazy (`GridView.builder` or `PageView.builder`), precache only neighbours, and then lower `maximumSizeBytes` in `main()` if the budget is still too high.
code
dart · 29 linesimport 'package:flutter/material.dart';
class ListingPhotoStrip extends StatelessWidget {
const ListingPhotoStrip({super.key, required this.urls});
final List<String> urls;
@override
Widget build(BuildContext context) {
final dpr = MediaQuery.devicePixelRatioOf(context);
const width = 280.0;
return SizedBox(
height: 200,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: urls.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(right: 8),
child: Image.network(
urls[index],
width: width,
fit: BoxFit.cover,
cacheWidth: (width * dpr).round(),
),
),
),
);
}
}go deeper
Know that Flutter caches decoded images in memory and that big photos shown at small sizes waste a lot of it.
Explain the default limits, the difference between cached and live images, and why decode size matters more than file size.
Fix a gallery OOM in order: decode size, lazy building, sparse precaching, cache limits and eviction, and confirm with oversized-image highlighting and cache counters.
Set per-device memory budgets for image-heavy features and make the server's image renditions part of that budget.
## What ImageCache actually holds Every `ImageProvider` resolves through the global cache, `PaintingBinding.instance.imageCache` (also reachable as the top-level `imageCache`). It tracks images in three states: | State | Meaning | Counted against the limits | |---|---|---| | **pending** | still loading or decoding | no | | **cached** (keep-alive) | completed, kept for reuse, least recently used first out | **yes** | | **live** | completed and still listened to, for example by an `Image` on screen | no | The limits apply to the **cached** part: - `maximumSize`, default **1000** entries; - `maximumSizeBytes`, default **100 MiB** (`100 << 20`). When either is exceeded, the least recently used entries are evicted. An image whose decoded size alone exceeds `maximumSizeBytes` is **not cached at all**, although it can still be live while displayed. ## Why the limits do not save the gallery A decoded image costs width × height × 4 bytes. A 4000 × 3000 listing photo is about 46 MiB. The limits cannot help in three common situations: 1. **Everything visible is live.** A detail page with a horizontal strip of twelve full-resolution photos holds all twelve as live images, over 500 MiB, and none of it counts toward `maximumSizeBytes`. 2. **Non-lazy lists build everything.** A `Column` or `ListView(children: ...)` of forty photos creates forty `Image` widgets, and each starts loading at once. 3. **Other holders keep images alive.** Any code that resolves a provider and keeps its listener, for example a custom precache that never removes its listener, pins the image in memory. On a platform memory-pressure signal, `PaintingBinding.handleMemoryPressure` calls `imageCache.clear()`. That drops pending and cached entries but, as the documentation says, **not live ones**, because clearing them would not free memory that widgets still hold. ## A fix, in order of impact 1. **Decode smaller.** Give every `Image.network` a `cacheWidth` computed from its on-screen width times the device pixel ratio. A 360-pixel-wide thumbnail of that photo takes about 0.4 MB instead of 46 MiB, a reduction of roughly a hundred times. 2. **Build lazily.** Use `GridView.builder`, `ListView.builder` or `PageView.builder`, so only the photos near the viewport exist, and their images stop being live when they scroll away. 3. **Precache sparingly.** Precache the next and previous photo in a viewer, not the whole listing. 4. **Right-size the cache.** If the app still keeps too much, lower the limits early in `main()`: ```dart void main() { WidgetsFlutterBinding.ensureInitialized(); PaintingBinding.instance.imageCache.maximumSizeBytes = 60 << 20; runApp(const EstateApp()); } ``` 5. **Evict what you will not revisit.** When the user leaves a listing, evict its photos with the same provider the widgets used (for a `cacheWidth` image that is the `ResizeImage`, since a bare `NetworkImage(url).evict()` targets a different key), or call `imageCache.clear()`. Evicting only removes cache entries; images still on screen stay live. 6. **Ask the server for smaller files.** Decoding smaller saves memory; downloading smaller also saves bandwidth and decode time. ## Deferred loading helps, but only while flinging Each `Image` wraps its provider in a `ScrollAwareImageProvider`, which postpones starting a load while the list scrolls at high velocity and skips it if the widget is gone by the time scrolling slows. That keeps a fast fling from starting forty downloads, but it does nothing for photos the user stops on, so it complements decode sizing rather than replacing it. ## A budget, worked through For a phone where the gallery should stay under about 150 MB of image memory: - 12 visible thumbnails at 360 × 270: about 4.7 MB live. - 1 full-screen photo at 1170 × 878: about 4 MB live. - 2 precached neighbours at the same size: about 8 MB. - the rest of the cache: capped by `maximumSizeBytes`. The same screen with undecoded 4000 × 3000 originals would need nearly 600 MB for the thumbnails alone. ## Tools for confirming it - `debugInvertOversizedImages = true` highlights images decoded much larger than displayed and logs both sizes. - `imageCache.currentSizeBytes`, `liveImageCount` and `pendingImageCount` give quick numbers in a debug overlay or log. - Heap snapshots and memory timelines for deeper leaks are a separate diagnostic workflow. ## Replacing the cache entirely A custom `WidgetsFlutterBinding` subclass can override `PaintingBinding.createImageCache()` to return an `ImageCache` subclass, for example to log evictions. That is rarely necessary; the settings above cover almost every gallery.
- Why doesn't lowering maximumSizeBytes to 50 MiB stop a screen showing twelve full-resolution photos from crashing?The byte limit governs only the cache of completed, unused images. The twelve photos on screen are live, still listened to by their `Image` widgets, and live images are not counted or evicted. The fix is to decode them smaller with `cacheWidth` and to build the strip lazily.
- What does Flutter do with the image cache when the OS reports memory pressure?`PaintingBinding.handleMemoryPressure` calls `imageCache.clear()`, which evicts pending and cached entries. Live images stay, because widgets still hold them, so clearing them would not free memory. Reducing what is live is up to the app.
saying these in an interview costs you the question
- maximumSizeBytes caps all image memory, including images on screen.
- A 2 MB JPEG uses about 2 MB of memory once displayed.
- imageCache.clear() frees the photos currently visible.
- An image larger than maximumSizeBytes is cached anyway and evicts everything else.
- Raising maximumSizeBytes fixes an out-of-memory crash.