[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"articolo-ottimizzare-le-immagini-in-flutter-imagecache-resizeimage-e-precaching":3,"comments-article-ottimizzare-le-immagini-in-flutter-imagecache-resizeimage-e-precaching":27},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},135,"Ottimizzare le immagini in Flutter: ImageCache, ResizeImage e precaching","ottimizzare-le-immagini-in-flutter-imagecache-resizeimage-e-precaching","Le immagini sono spesso la prima causa di memoria alta e jank nelle app Flutter. Vediamo come funziona la pipeline di decodifica, come usare cacheWidth, ResizeImage, precacheImage e come configurare ImageCache per risparmiare decine di MB.","Le immagini sono quasi sempre l'elemento più pesante di un'app Flutter: pesano sul bundle, sulla rete e — soprattutto — sulla RAM. Una schermata con una griglia di foto può passare da 300 MB a 80 MB di memoria con tre righe di codice, se si capisce come funziona davvero la pipeline di decodifica.\n\nIn questo articolo vediamo il percorso che un'immagine compie da file a pixel sullo schermo, e tutte le leve che Flutter mette a disposizione per renderlo efficiente.\n\n## Perché le immagini pesano più di quanto sembra\n\nIl punto chiave è che **la dimensione del file non ha nulla a che vedere con la memoria occupata**. Un JPEG compresso di 400 KB, una volta decodificato, diventa un bitmap non compresso in memoria. La formula è semplice:\n\n```\nmemoria (byte) = larghezza_px × altezza_px × 4\n```\n\nI 4 byte sono i canali RGBA. Quindi:\n\n- un'immagine 1200×1200 occupa `1200 × 1200 × 4` = **5,76 MB** in RAM\n- venti immagini del genere in una griglia = **115 MB**, anche se visualizzate in riquadri da 100×100 logici\n\nIl disastro è che quei 100×100 logici su un telefono con `devicePixelRatio` 3 richiedono solo 300×300 pixel fisici, cioè **360 KB**. Stiamo buttando via il 94% della memoria allocata.\n\n## La pipeline: ImageProvider, ImageCache e ui.Image\n\nCapire i quattro attori in gioco rende tutto il resto ovvio:\n\n1. **`ImageProvider`** (`AssetImage`, `NetworkImage`, `FileImage`, `MemoryImage`): non contiene pixel, è solo una *descrizione* di dove stanno i byte. Per questo è importante che implementi correttamente `==` e `hashCode`: due provider uguali devono finire sulla stessa chiave di cache.\n2. **`ImageCache`**: una mappa in memoria, globale, accessibile via `PaintingBinding.instance.imageCache`. Di default conserva fino a **1000 immagini** e **100 MiB** di pixel decodificati.\n3. **Il codec** (`dart:ui`): decodifica i byte compressi in un `ui.Image`. La decodifica avviene in modo asincrono, ma l'allocazione di memoria è reale e immediata.\n4. **`RenderImage`**: disegna il `ui.Image` nel riquadro assegnato dal layout, scalandolo con la GPU.\n\nIl passaggio critico è il punto 3: **Flutter decodifica alla risoluzione nativa del file**, a meno che non gli diciamo esplicitamente il contrario. Il ridimensionamento fatto dal layout (`BoxFit.cover` in un `SizedBox` piccolo) avviene *dopo*, sulla GPU, e non risparmia un solo byte di RAM.\n\n## Prima leva: cacheWidth e cacheHeight\n\nI costruttori `Image.asset`, `Image.network`, `Image.file` e `Image.memory` accettano `cacheWidth` e `cacheHeight`. Dietro le quinte avvolgono il provider in un `ResizeImage`, che istruisce il codec a decodificare direttamente a quella dimensione.\n\n```dart\nImage.network(\n  photo.url,\n  width: 100,\n  height: 100,\n  fit: BoxFit.cover,\n  \u002F\u002F Attenzione: valori in PIXEL FISICI, non logici\n  cacheWidth: 300,\n  cacheHeight: 300,\n)\n```\n\nL'errore più comune è passare la dimensione logica del widget. Su un display con `devicePixelRatio` 3 otterremmo un'immagine sgranata. La soluzione corretta è calcolare il valore a runtime:\n\n```dart\nclass ScaledImage extends StatelessWidget {\n  const ScaledImage({\n    super.key,\n    required this.url,\n    required this.size,\n  });\n\n  final String url;\n  final Size size;\n\n  @override\n  Widget build(BuildContext context) {\n    final dpr = MediaQuery.devicePixelRatioOf(context);\n\n    return Image.network(\n      url,\n      width: size.width,\n      height: size.height,\n      fit: BoxFit.cover,\n      cacheWidth: (size.width * dpr).round(),\n      cacheHeight: (size.height * dpr).round(),\n    );\n  }\n}\n```\n\nUn dettaglio importante: specificando **entrambi** i valori, il codec può deformare l'immagine se le proporzioni non coincidono. Nella maggior parte dei casi conviene indicarne uno solo — tipicamente quello del lato vincolante — e lasciare che l'altro segua l'aspect ratio.\n\n## ResizeImage: controllo fine sul provider\n\nQuando si usa un `ImageProvider` direttamente (ad esempio in un `DecorationImage`, in un `CircleAvatar` o con `precacheImage`), `cacheWidth` non è disponibile: bisogna usare `ResizeImage` esplicitamente.\n\n```dart\nfinal provider = ResizeImage(\n  NetworkImage(user.avatarUrl),\n  width: 128,\n  policy: ResizeImagePolicy.fit,\n  allowUpscaling: false,\n);\n\nContainer(\n  decoration: BoxDecoration(\n    shape: BoxShape.circle,\n    image: DecorationImage(image: provider, fit: BoxFit.cover),\n  ),\n);\n```\n\nDue parametri meritano attenzione:\n\n- **`policy`**: con `ResizeImagePolicy.exact` (default) larghezza e altezza indicate sono quelle finali esatte; con `ResizeImagePolicy.fit` l'immagine viene ridimensionata in modo da *contenere* il riquadro, mantenendo le proporzioni. `fit` è quasi sempre la scelta giusta per le thumbnail.\n- **`allowUpscaling`**: lasciandolo a `false` si evita di sprecare memoria ingrandendo immagini già piccole.\n\n`ResizeImage` modifica anche la **chiave di cache**: `ResizeImage(NetworkImage(url), width: 128)` e `ResizeImage(NetworkImage(url), width: 512)` sono due voci distinte. È il comportamento desiderato, ma significa anche che usare dimensioni diverse per la stessa immagine in schermate diverse raddoppia l'occupazione. Meglio standardizzare pochi \"tagli\" (thumbnail, card, full screen) e riutilizzarli.\n\n## Asset a risoluzioni multiple\n\nPer gli asset locali Flutter ha un meccanismo nativo: le varianti per densità. La struttura è questa:\n\n```\nassets\u002F\n  images\u002F\n    hero.png        # 1x\n    2.0x\u002Fhero.png\n    3.0x\u002Fhero.png\n```\n\nE nel `pubspec.yaml` si dichiara **solo** il percorso base:\n\n```yaml\nflutter:\n  assets:\n    - assets\u002Fimages\u002Fhero.png\n```\n\nA runtime `AssetImage` sceglie la variante corretta in base al `devicePixelRatio` del dispositivo. Su un tablet a 1x non verrà mai decodificata la versione 3x: un risparmio automatico di memoria e di tempo di decodifica.\n\n> Suggerimento: dichiarando una cartella (`- assets\u002Fimages\u002F`) invece dei singoli file, le sottocartelle `2.0x`\u002F`3.0x` vengono comunque gestite correttamente, ma si perde la possibilità di escludere asset inutilizzati.\n\n## precacheImage: eliminare il flash al primo frame\n\nQuando un'immagine non è in cache, il primo frame in cui compare mostra uno spazio vuoto. Per immagini critiche (hero di un onboarding, sfondo della home) conviene scaldare la cache in anticipo.\n\n```dart\nclass HomePage extends StatefulWidget {\n  const HomePage({super.key});\n\n  @override\n  State\u003CHomePage> createState() => _HomePageState();\n}\n\nclass _HomePageState extends State\u003CHomePage> {\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    \u002F\u002F Qui il context ha già accesso a MediaQuery: il provider\n    \u002F\u002F risolverà la variante di densità corretta.\n    precacheImage(const AssetImage('assets\u002Fimages\u002Fhero.png'), context);\n  }\n\n  @override\n  Widget build(BuildContext context) => const Scaffold(\n        body: Image(image: AssetImage('assets\u002Fimages\u002Fhero.png')),\n      );\n}\n```\n\n`precacheImage` va chiamato in `didChangeDependencies`, non in `initState`, perché ha bisogno di un `BuildContext` con `MediaQuery` disponibile. Restituisce un `Future` che si completa quando l'immagine è decodificata: si può usare anche per bloccare una splash screen finché gli asset essenziali non sono pronti.\n\nAttenzione a non abusarne: precaricare 50 immagini all'avvio significa allocare decine di MB prima ancora che l'utente veda qualcosa.\n\n## Configurare e governare l'ImageCache\n\nI default (1000 immagini, 100 MiB) sono pensati per un telefono di fascia media. Su app con molte immagini grandi, o su dispositivi con poca RAM, vale la pena regolarli:\n\n```dart\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  final cache = PaintingBinding.instance.imageCache;\n  cache.maximumSizeBytes = 60 \u003C\u003C 20; \u002F\u002F 60 MiB\n  cache.maximumSize = 200;           \u002F\u002F numero massimo di voci\n\n  runApp(const MyApp());\n}\n```\n\nAlcune operazioni utili sulla cache:\n\n```dart\nfinal cache = PaintingBinding.instance.imageCache;\n\n\u002F\u002F Rimuove una specifica immagine (es. l'avatar è cambiato sul server)\ncache.evict(NetworkImage(user.avatarUrl));\n\n\u002F\u002F Svuota le immagini non più referenziate\ncache.clear();\n\n\u002F\u002F Svuota anche quelle attualmente \"vive\" (in uso da un widget)\ncache.clearLiveImages();\n\n\u002F\u002F Diagnostica\ndebugPrint('${cache.currentSize} immagini, '\n    '${(cache.currentSizeBytes \u002F 1024 \u002F 1024).toStringAsFixed(1)} MB');\n```\n\nUn pattern utile è reagire alla pressione di memoria del sistema operativo. `WidgetsBindingObserver` espone `didHaveMemoryPressure`:\n\n```dart\nclass _AppState extends State\u003CApp> with WidgetsBindingObserver {\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n  }\n\n  @override\n  void didHaveMemoryPressure() {\n    PaintingBinding.instance.imageCache.clear();\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n}\n```\n\nNota: `ImageCache` è una cache **in memoria**, non su disco. `Image.network` non salva nulla sul filesystem, quindi al riavvio dell'app tutto viene riscaricato. Per la persistenza serve un livello aggiuntivo (come `cached_network_image`, che internamente combina cache su disco e `ImageCache`).\n\n## Stati di caricamento: frameBuilder, loadingBuilder, errorBuilder\n\nUn'immagine che appare di colpo è percepita come un glitch. Il widget `Image` offre tre hook:\n\n```dart\nImage.network(\n  url,\n  fit: BoxFit.cover,\n  \u002F\u002F Chiamato a ogni frame disponibile: ideale per un fade-in\n  frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {\n    if (wasSynchronouslyLoaded) return child;\n    return AnimatedOpacity(\n      opacity: frame == null ? 0 : 1,\n      duration: const Duration(milliseconds: 250),\n      curve: Curves.easeOut,\n      child: child,\n    );\n  },\n  \u002F\u002F Solo per immagini di rete: progresso del download\n  loadingBuilder: (context, child, progress) {\n    if (progress == null) return child;\n    final value = progress.expectedTotalBytes != null\n        ? progress.cumulativeBytesLoaded \u002F progress.expectedTotalBytes!\n        : null;\n    return Center(child: CircularProgressIndicator(value: value));\n  },\n  errorBuilder: (context, error, stackTrace) =>\n      const Icon(Icons.broken_image_outlined),\n)\n```\n\nIl flag `wasSynchronouslyLoaded` è prezioso: se l'immagine era già in cache non ha senso animare un fade-in, risulterebbe solo un lampeggio.\n\nUn altro parametro poco noto è **`gaplessPlayback: true`**: quando l'URL cambia, mantiene a schermo l'immagine precedente finché la nuova non è pronta, invece di mostrare un buco bianco. Perfetto per carousel e cambio avatar.\n\n## Immagini in liste e griglie\n\nNelle liste scrollabili si sommano tutti i problemi. Regole pratiche:\n\n- **Sempre `cacheWidth`\u002F`cacheHeight`** calcolati sulla cella, mai sull'immagine originale.\n- **Usare `ListView.builder`\u002F`GridView.builder`** e non costruire tutte le celle in anticipo.\n- **Tenere basso il `cacheExtent`** quando le celle contengono immagini pesanti: un `cacheExtent` di 1000 px su una lista di foto significa decodificare molte immagini fuori schermo.\n- **Evitare `AutomaticKeepAlive`** nelle celle con immagini: mantenere vivi i widget fuori schermo impedisce all'`ImageCache` di liberare le voci \"live\".\n- **Costruttori `const`** dove possibile, per evitare che un rebuild ricrei `ImageProvider` non uguali e quindi cache miss.\n\n```dart\nGridView.builder(\n  cacheExtent: 200,\n  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(\n    crossAxisCount: 3,\n    mainAxisSpacing: 4,\n    crossAxisSpacing: 4,\n  ),\n  itemCount: photos.length,\n  itemBuilder: (context, index) {\n    final dpr = MediaQuery.devicePixelRatioOf(context);\n    final cellWidth = MediaQuery.sizeOf(context).width \u002F 3;\n    return Image.network(\n      photos[index].thumbUrl,\n      fit: BoxFit.cover,\n      cacheWidth: (cellWidth * dpr).round(),\n      filterQuality: FilterQuality.medium,\n    );\n  },\n)\n```\n\nPiccola nota su `filterQuality`: il default `FilterQuality.medium` (da Flutter 3.27 in poi, al posto del vecchio `low`) è un buon compromesso. `high` applica un filtro bicubico costoso: usatelo solo su singole immagini grandi, mai in griglie.\n\n## Scovare le immagini sovradimensionate\n\nFlutter ha un flag di debug dedicato che capovolge verticalmente ogni immagine decodificata a una risoluzione molto maggiore di quella visualizzata. L'effetto è impossibile da ignorare:\n\n```dart\nimport 'package:flutter\u002Frendering.dart';\n\nvoid main() {\n  debugInvertOversizedImages = true;\n  runApp(const MyApp());\n}\n```\n\nOgni immagine \"capovolta\" sullo schermo è un errore da correggere con `cacheWidth`. In console compare anche un warning con dimensione reale, dimensione visualizzata e spreco in KB. La soglia è configurabile con `debugImageOverheadAllowance`.\n\nIn **DevTools**, inoltre:\n\n- la tab **Memory** permette di tracciare le allocazioni e vedere crescere la heap mentre si scorre una lista;\n- la tab **Performance** evidenzia i frame in cui la decodifica causa jank;\n- il **Flutter Inspector** espone l'opzione *Invert Oversized Images* senza ricompilare.\n\n## Peso del bundle: formati e tree shaking\n\nUltimo fronte, quello del download iniziale:\n\n- **WebP** è supportato nativamente da Flutter (anche animato) e riduce tipicamente del 25-35% rispetto a PNG\u002FJPEG. AVIF invece **non** è supportato dal motore: serve un plugin.\n- Per le icone, `flutter build` in modalità release applica di default il **tree shaking dei font di icone**: solo i glyph effettivamente usati finiscono nel binario. Questo funziona però solo con `IconData` costanti; costruire icone dinamicamente (`IconData(codePoint)`) disattiva l'ottimizzazione e produce un warning.\n- Per illustrazioni e loghi, la grafica vettoriale (SVG precompilato) pesa pochi KB e scala a qualsiasi densità.\n- Le immagini caricate dall'utente (foto da fotocamera o galleria) vanno **compresse prima dell'upload**: un pacchetto come `flutter_image_compress` esegue il lavoro su codice nativo senza bloccare l'isolate principale.\n\n## Checklist finale\n\nPrima di considerare chiusa una schermata con immagini, verificate:\n\n1. Ogni `Image` in lista o griglia ha `cacheWidth`\u002F`cacheHeight` calcolati con il `devicePixelRatio`.\n2. I provider usati in `DecorationImage`\u002F`CircleAvatar` sono avvolti in `ResizeImage`.\n3. Gli asset locali hanno le varianti `2.0x` e `3.0x`.\n4. Le immagini critiche sono precaricate con `precacheImage` in `didChangeDependencies`.\n5. `maximumSizeBytes` dell'`ImageCache` è dimensionato sul target reale dell'app.\n6. Sono gestiti `errorBuilder` e un placeholder\u002Ffade-in.\n7. `debugInvertOversizedImages = true` non mostra nessuna immagine capovolta.\n8. Le immagini di rete passano da una cache su disco, non da `Image.network` puro.\n\nSono otto controlli che richiedono mezza giornata di lavoro e che, su un'app ricca di contenuti visuali, fanno spesso la differenza tra uno scroll a 60 fps e crash per OOM sui dispositivi di fascia bassa.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F44512105-7700-400c-97e1-c1468b610a3d.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1606280885456-a521058a5efe?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTI2NDE3MzR8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"张 岱","https:\u002F\u002Funsplash.com\u002F@zhang_d","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fperson-holding-black-android-smartphone-8fLQseh7gEQ",null,"published","2026-10-22T04:02:15+00:00","Ottimizzare le immagini in Flutter: cache e memoria","Guida pratica a ImageCache, cacheWidth, ResizeImage e precacheImage per ridurre memoria e jank nelle app Flutter piene di immagini.",{"id":21,"name":22,"slug":23},3,"Best practice","best-practice",{"id":25,"name":26},1,"Flutter Bot",[],1792664879178]