Flutter Image Loading and Caching: From Image.network to CachedNetworkImage
How Flutter caches images in memory, how FadeInImage and precacheImage smooth the loading experience, and when to reach for disk caching with cached_network_image.
Published on • October 3, 2026
AI Assistant

A feed full of remote images is the most common performance trap in Flutter apps: scroll fast, watch frames drop, see placeholders pop in. The good news is that Flutter already ships a well-designed in-memory image cache — you just have to know it exists and how to work with it.
Sources: Flutter cookbook: network images, ImageCache API, FadeInImage API, cached_network_image on pub.dev.
The built-in path: Image.network
Image.network(
'https://picsum.photos/250?image=9',
loadingBuilder: (context, child, progress) =>
progress == null ? child : const CircularProgressIndicator(),
errorBuilder: (context, error, stack) => const Icon(Icons.broken_image),
fit: BoxFit.cover,
)
Three details from the API docs matter more than they look:
- All network images are cached regardless of HTTP headers. Flutter ignores
Cache-Controland does its own thing. cacheWidth/cacheHeightdecode at a target size. A 4000×3000 avatar rendered at 100×100 is pure memory waste; these parameters shrink the decoded bitmap, not just the layout.loadingBuilder,errorBuilder,frameBuildergive you the three states (loading, failed, first frame) without wrapping the widget in aFutureBuilder.
The in-memory cache: LRU, 1000 images / 100 MB
Every Image goes through PaintingBinding.imageCache, a shared LRU cache capped at 1000 entries and 100 MB by default. Once an image is in it, re-displaying it costs no network and no decode. Exceed the limits and the least-recently-used entries are evicted — which is why fast scrolling can evict an image you’re about to show again.
You can tune it globally:
PaintingBinding.instance.imageCache
..maximumSize = 2000
..maximumSizeBytes = 200 * 1024 * 1024;
Or prefetch likely-needed images with precacheImage:
precacheImage(NetworkImage(url), context, onError: (e, s) {/* ignore */});
The returned Future completes even on failure — a subtle trap if you await it to decide whether the image exists. And don’t precache aggressively: pinning large images is a documented out-of-memory risk.
Smooth loading with FadeInImage
Instant pops look broken. The cookbook’s fix is a fade from a tiny placeholder:
FadeInImage.memoryNetwork(
placeholder: kTransparentImage, // 1×1 transparent GIF bytes
image: url,
fadeInDuration: const Duration(milliseconds: 300),
fit: BoxFit.cover,
)
Defaults are a 700 ms fade-in and 300 ms fade-out. FadeInImage.assetNetwork does the same from an asset placeholder. Pair with a fixed height on the parent so rows don’t jump as images arrive.
Disk caching: when memory isn’t enough
The built-in cache is memory only — restart the app and everything re-downloads. That’s fine for a session; it’s unacceptable for an offline-first or data-conscious app. That’s the gap cached_network_image (4.0.4, Baseflow/Stream) fills:
CachedNetworkImage(
imageUrl: url,
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
memCacheWidth: 400, // decode-size hint
)
Under the hood it stores and retrieves files with flutter_cache_manager, adding a persistent disk layer, plus progress reporting (progressIndicatorBuilder) and an imageBuilder for decoration/filter wrappers. You can also use it as a plain ImageProvider: Image(image: CachedNetworkImageProvider(url)).
Two caveats from the README: web support is minimal and includes no caching (use the browser’s own HTTP cache there), and failed loads sometimes look like crashes to debuggers/crash reporters even when the app is fine.
Assets are cached too — and they’re resolution-aware
Image.asset('images/cat.png') uses AssetImage, which picks 2x/, 3x/ variants automatically when scale isn’t pinned. Assets must be declared in pubspec.yaml. They’re the right choice for anything in your control: placeholders, icons, empty-state illustrations — no network, no flicker.
A practical checklist
- Set
cacheWidth/memCacheWidthfor list/grid images — the single biggest memory win. - Always supply
errorBuilder— remote images do fail, and a broken icon beats a red screen. - Use
FadeInImageorloadingBuilderso nothing pops; reserveshimmerpackages for lists that need it. - Tune the global
ImageCachedeliberately if your app is image-heavy, and monitor with DevTools’ memory chart. - Add
cached_network_imagewhen you need offline/repeat views; skip it on web. precacheImageonly what’s next, not what’s “probably somewhere” — memory is the scarcest resource on a phone.
Get these right and the classic Flutter complaints — janky scrolling, flickering feeds, re-downloading on every launch — mostly disappear, because the framework was already doing the heavy lifting.