In Flutter, what do cacheWidth and cacheHeight do on Image.network, and why can they stop a photo gallery from running out of memory?
answer
- decoded size = width × height × 4
- file size is not memory size
- physical pixels: logical × DPR
- wraps the provider in ResizeImage
- ignored for network images on web
basics
~20 sThey make the engine decode the image at the given pixel size instead of full resolution. Decoded images cost width × height × 4 bytes, so a 4000 × 3000 photo takes about 48 MB however small its JPEG; decoding at thumbnail size cuts that sharply.
solid answer
~40 sAn image is stored decoded, at about 4 bytes per pixel, so memory follows the pixel count, not the download size: a 4000 × 3000 listing photo costs about 48 MB in the `ImageCache` even as a 1 MB JPEG. `cacheWidth` and `cacheHeight` wrap the provider in a `ResizeImage`, and the engine then decodes and caches the image at that size. The values are **physical** pixels, so I pass the displayed logical width times `MediaQuery.devicePixelRatioOf(context)`. Setting only one keeps the aspect ratio, and by default the image is never upscaled beyond its intrinsic size. Layout is unaffected: `width`, `height` and `fit` still decide the painted size. For network images on the web the parameters are ignored, because the browser decodes.
code
dart · 20 linesimport 'package:flutter/material.dart';
class ListingThumbnail extends StatelessWidget {
const ListingThumbnail({super.key, required this.url});
static const double logicalWidth = 120;
final String url;
@override
Widget build(BuildContext context) {
final dpr = MediaQuery.devicePixelRatioOf(context);
return Image.network(
url,
width: logicalWidth,
height: logicalWidth * 3 / 4,
fit: BoxFit.cover,
cacheWidth: (logicalWidth * dpr).round(),
);
}
}go deeper
Know that big photos cost memory once decoded and that cacheWidth or cacheHeight make Flutter decode them smaller.
Explain width × height × 4, ResizeImage, one-dimension aspect preservation, no upscaling by default, and the logical-to-physical conversion.
Diagnose a gallery OOM from decoded sizes, apply per-context decode sizes, use the oversized-image flag, and push the server toward smaller renditions.
Set image-size contracts between the app and the image service so clients never download or decode more than they display.
## Why a gallery runs out of memory A JPEG or WebP file is compressed. To draw it, the engine **decodes** it into an uncompressed bitmap, and Flutter's `ImageCache` accounts for each decoded image as **width × height × 4 bytes**. Download size is therefore a poor guide to memory: | Photo | File size (typical) | Decoded size | |---|---|---| | 4000 × 3000 listing photo | about 1-3 MB | 48,000,000 bytes, about 46 MiB | | same photo at 1080 × 810 | — | about 3.5 MB | | same photo at 360 × 270 | — | about 0.4 MB | A real-estate gallery that shows twenty full-resolution photos in a grid of small thumbnails asks for close to a gigabyte of decoded pixels, while only a fraction of those pixels ever reach the screen. On phones with little memory the operating system kills the app, often without a Dart error. ## What the parameters do All four `Image` constructors accept **`cacheWidth`** and **`cacheHeight`**. When either is set, the constructor wraps the provider in a **`ResizeImage`** (through `ResizeImage.resizeIfNeeded`), and the engine decodes directly to the requested size. The smaller bitmap is what the `ImageCache` stores. The rules, from `ResizeImage` and its default `ResizeImagePolicy.exact`: - **Both set**: the output has exactly that width and height, like `BoxFit.fill`, even if the aspect ratio differs. - **One set**: that dimension is used and the other follows the original aspect ratio. This is usually what you want. - **No upscaling by default**: `allowUpscaling` is `false`, so asking for 2000 pixels from a 1200-pixel image gives 1200. - **Separate cache entries**: the `ResizeImage` key includes the target size, so the same URL at 360 and at 1080 pixels is cached as two different images. ## Physical, not logical, pixels Layout sizes in Flutter are **logical** pixels; decoding happens in **physical** pixels. A thumbnail 120 logical pixels wide on a 3.0 device covers 360 physical pixels, so: ```dart final dpr = MediaQuery.devicePixelRatioOf(context); final cacheWidth = (120 * dpr).round(); ``` Passing the logical width makes the image blurry on high-density screens; passing the full photo width saves nothing. ## What does not change `cacheWidth` and `cacheHeight` only change the **decoded** size. The painted size still comes from layout: the constraints, `width`, `height` and `fit`. They also do not shrink the **download**: the full file is still fetched. Serving smaller renditions from the server saves bandwidth as well, and is the better fix where you control the server. ## Finding oversized images In debug builds, `debugInvertOversizedImages = true` (from `package:flutter/painting.dart`) paints images inverted and flipped when their decoded size wastes at least 128 KB compared with their display size, and logs the display and decode sizes. DevTools' inspector exposes the same switch as "Highlight oversized images". The fix it suggests is a smaller file where possible, otherwise `cacheWidth` and `cacheHeight`. ## Choosing a decode size per surface The same listing photo usually appears at several sizes, and each surface should decode for its own size: | Surface | Logical width | `cacheWidth` on a 3.0 device | |---|---|---| | Search-results grid thumbnail | 120 | 360 | | Listing card in a feed | full width, about 390 | about 1170 | | Full-screen viewer | screen width | screen width × 3 | | Pinch-zoom viewer | a multiple of screen width | larger still, or none | Compute the value from the layout rather than hard-coding it, for example with a `LayoutBuilder` for grid cells, so tablets and foldables decode for their real cell size. Round to whole pixels; `cacheWidth` is an `int`. ## Web and other limits 1. For `Image.network` on the web, both parameters are **ignored**: the browser decodes the image and does not support custom decode sizes. 2. Animated images decode every frame at the reduced size too, which is usually welcome. 3. A detail view that needs the full resolution should use its own, larger `cacheWidth`, or none, and accept that this one image is expensive.
- If the grid thumbnail and the full-screen viewer show the same URL with different cacheWidth values, how many decoded images are cached?Two. Each `cacheWidth` wraps the `NetworkImage` in a `ResizeImage` whose key includes the target size, so the 360-pixel thumbnail and the 1080-pixel viewer image are separate `ImageCache` entries. That is usually fine, but precaching must use the same size as the widget that will show it.
- Does cacheWidth reduce the bytes downloaded for a listing photo?No. The full file is still downloaded; only decoding happens at the smaller size, which saves memory and decode time. To save bandwidth, request a smaller rendition from the server, for example through an image service that resizes by URL parameter.
- Why does a thumbnail look blurry when cacheWidth is set to its layout width?Layout widths are logical pixels, while decoding is in physical pixels. On a 3.0 device a 120-logical-pixel thumbnail needs 360 physical pixels; decoding at 120 and then stretching it three times blurs it. Multiply by `MediaQuery.devicePixelRatioOf(context)`.
Keeping a full-resolution photo for a thumbnail is like printing a poster to slip into a wallet: the wallet only shows a corner of it, but you still carry the whole sheet. cacheWidth asks the print shop for a wallet-sized print in the first place, and the viewing space in the wallet stays the same.
saying these in an interview costs you the question
- A 1 MB JPEG takes about 1 MB of memory once displayed.
- cacheWidth is in logical pixels, like width.
- cacheWidth changes the size the image is painted at on screen.
- cacheWidth reduces how many bytes are downloaded.
- cacheWidth works the same for Image.network on the web.