Skip to content
Blog

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:

  1. All network images are cached regardless of HTTP headers. Flutter ignores Cache-Control and does its own thing.
  2. cacheWidth / cacheHeight decode 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.
  3. loadingBuilder, errorBuilder, frameBuilder give you the three states (loading, failed, first frame) without wrapping the widget in a FutureBuilder.

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

  1. Set cacheWidth/memCacheWidth for list/grid images — the single biggest memory win.
  2. Always supply errorBuilder — remote images do fail, and a broken icon beats a red screen.
  3. Use FadeInImage or loadingBuilder so nothing pops; reserve shimmer packages for lists that need it.
  4. Tune the global ImageCache deliberately if your app is image-heavy, and monitor with DevTools’ memory chart.
  5. Add cached_network_image when you need offline/repeat views; skip it on web.
  6. precacheImage only 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.