[{"data":1,"prerenderedAt":88},["ShallowReactive",2],{"tutorial-ottimizzare-il-bundle-di-flutter-web-deferred-loading-wasm-e-caching":3,"comments-tutorial-ottimizzare-il-bundle-di-flutter-web-deferred-loading-wasm-e-caching":87},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":34},113,"Ottimizzare il bundle di Flutter Web: deferred loading, Wasm e caching","ottimizzare-il-bundle-di-flutter-web-deferred-loading-wasm-e-caching","Guida avanzata per ridurre il tempo di primo caricamento di un'app Flutter Web: misura della baseline, deferred loading delle feature, build WebAssembly, tree shaking degli asset, service worker e budget di dimensione in CI.","Su mobile il peso del bundle incide poco sull'esperienza: l'app è già installata. Sul web, invece, ogni kilobyte di `main.dart.js` è tempo che l'utente passa davanti a uno schermo bianco. Un'app Flutter Web non banale può facilmente superare i 3-4 MB di JavaScript, a cui si aggiungono il runtime CanvasKit, i font e le icone.\n\nIn questo tutorial avanzato vedremo una pipeline completa di ottimizzazione:\n\n- **misurare** la baseline in modo ripetibile (gzip\u002FBrotli, source map, Lighthouse);\n- **spezzare** il bundle con il deferred loading di Dart (`deferred as` + `loadLibrary()`), integrandolo con il router;\n- **scegliere il renderer** giusto e provare la compilazione **WebAssembly** (skwasm) con gli header COOP\u002FCOEP;\n- **snellire gli asset**: tree shaking delle icone, subset dei font, immagini moderne;\n- **governare la cache** con il bootstrap di Flutter e il service worker;\n- **bloccare le regressioni** con un performance budget nella CI.\n\n> Prerequisiti: Flutter 3.22 o successivo (nuovo `flutter_bootstrap.js`), Dart 3, familiarità con `go_router` e con la build release per il web.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F63423105-910b-4a0a-bf2a-455e930de365.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1610989001873-03968eed0f08?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTEwMDE5MzN8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Ferenc Almasi","https:\u002F\u002Funsplash.com\u002F@flowforfrank","https:\u002F\u002Funsplash.com\u002Fphotos\u002Ftext-vetW6Y89T3w","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=br9RL_DcrT4",{"youtube_id":17,"duration_seconds":18,"published_at":19},"br9RL_DcrT4",172,"2026-10-03T10:11:17+00:00","advanced",55,"3.x (3.22+ consigliato)","published","2026-10-03T04:32:13+00:00","Ottimizzare Flutter Web: deferred loading e build Wasm","Tutorial avanzato: riduci il bundle di Flutter Web con deferred loading, build WebAssembly, tree shaking asset, caching e budget di dimensione in CI.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,51,57,64,71,79],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},756,"Misurare la baseline: dimensioni reali e source map","Prima di ottimizzare serve un numero di riferimento **riproducibile**. Il peso che conta non è quello su disco ma quello trasferito in rete, cioè compresso con gzip o Brotli (quasi tutti i CDN servono Brotli).\n\nCompila in release abilitando le source map, poi misura:\n\n1. `flutter build web --release --source-maps`\n2. calcola la dimensione compressa dei file principali;\n3. usa `source-map-explorer` per capire **quali package** occupano spazio dentro `main.dart.js`.\n\nAnnota tre metriche: peso di `main.dart.js` (Brotli), peso totale di `build\u002Fweb`, e il **First Contentful Paint** misurato con Lighthouse in modalità *Mobile \u002F Slow 4G*. Senza questi tre numeri ogni ottimizzazione successiva è un'opinione.\n\n> Attenzione: non pubblicare le source map in produzione. Generale solo per l'analisi, oppure caricale su un servizio di error reporting e rimuovile dall'artefatto pubblico.","#!\u002Fusr\u002Fbin\u002Fenv bash\nset -euo pipefail\n\nflutter clean\nflutter build web --release --source-maps\n\necho \"== Dimensioni (raw \u002F gzip \u002F brotli) ==\"\nfor f in build\u002Fweb\u002Fmain.dart.js build\u002Fweb\u002Fflutter_bootstrap.js; do\n  raw=$(wc -c \u003C \"$f\")\n  gz=$(gzip -9 -c \"$f\" | wc -c)\n  br=$(brotli -q 11 -c \"$f\" | wc -c)\n  printf \"%-40s %8s KB  %8s KB  %8s KB\\n\" \\\n    \"$(basename \"$f\")\" $((raw\u002F1024)) $((gz\u002F1024)) $((br\u002F1024))\ndone\n\necho \"== Totale cartella build\u002Fweb ==\"\ndu -sh build\u002Fweb\n\n# Analisi per package (richiede Node.js)\nnpx source-map-explorer build\u002Fweb\u002Fmain.dart.js --no-border-checks\n\n# Rimuovi le source map prima del deploy pubblico\n# find build\u002Fweb -name '*.map' -delete","bash","Hai un report con il peso Brotli di main.dart.js e una treemap che mostra i package più pesanti (spesso intl, firebase, grafici o modelli generati).",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":49,"expected_result":50,"demo_url":42,"video_url":42},757,2,"Spezzare il bundle con il deferred loading di Dart","Dart supporta la **deferred loading**: una libreria importata con `deferred as` non finisce nel bundle principale ma in un chunk separato (`main.dart.js_1.part.js`), scaricato solo quando chiami `loadLibrary()`.\n\nRegole operative:\n\n- il candidato ideale è una **feature intera** (report con grafici, editor, scanner, area admin), non una singola classe;\n- tutti gli accessi alla libreria differita devono avvenire tramite il prefisso (`reports.ReportsPage()`), e solo **dopo** che `loadLibrary()` è completato;\n- `loadLibrary()` è idempotente: chiamarlo più volte restituisce lo stesso `Future` già risolto, quindi puoi usarlo anche come \"preload\";\n- attenzione ai **tipi condivisi**: se il bundle principale referenzia un tipo della libreria differita (per esempio in una firma di funzione), il compilatore lo riporta nel chunk principale annullando il guadagno. Tieni i modelli condivisi in un package comune.\n\nCreiamo un widget riutilizzabile `DeferredBuilder` che gestisce stato di caricamento ed errore, memorizzando il `Future` per evitare di ricaricarlo a ogni rebuild.","import 'package:flutter\u002Fmaterial.dart';\n\n\u002F\u002F\u002F Carica una libreria differita e costruisce la UI solo quando è pronta.\nclass DeferredBuilder extends StatefulWidget {\n  const DeferredBuilder({\n    super.key,\n    required this.loader,\n    required this.builder,\n    this.placeholder = const Center(child: CircularProgressIndicator()),\n  });\n\n  \u002F\u002F\u002F Tipicamente `() => reports.loadLibrary()`.\n  final Future\u003Cvoid> Function() loader;\n  final WidgetBuilder builder;\n  final Widget placeholder;\n\n  @override\n  State\u003CDeferredBuilder> createState() => _DeferredBuilderState();\n}\n\nclass _DeferredBuilderState extends State\u003CDeferredBuilder> {\n  late Future\u003Cvoid> _future = widget.loader();\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder\u003Cvoid>(\n      future: _future,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState != ConnectionState.done) {\n          return widget.placeholder;\n        }\n        if (snapshot.hasError) {\n          return _RetryView(\n            message: 'Impossibile caricare il modulo.',\n            onRetry: () => setState(() => _future = widget.loader()),\n          );\n        }\n        return widget.builder(context);\n      },\n    );\n  }\n}\n\nclass _RetryView extends StatelessWidget {\n  const _RetryView({required this.message, required this.onRetry});\n  final String message;\n  final VoidCallback onRetry;\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Text(message),\n          const SizedBox(height: 12),\n          FilledButton(onPressed: onRetry, child: const Text('Riprova')),\n        ],\n      ),\n    );\n  }\n}","dart","Hai un widget generico pronto a ospitare qualunque schermata differita, con gestione di loading, errore e retry.",{"id":52,"position":28,"title":53,"body":54,"code_snippet":55,"code_language":49,"expected_result":56,"demo_url":42,"video_url":42},758,"Integrare i chunk differiti con go_router e precaricarli","Ora colleghiamo il `DeferredBuilder` al router. L'import differito va dichiarato nel file delle rotte; la `builder` della rotta restituisce il `DeferredBuilder`.\n\nDue accorgimenti che fanno la differenza percepita:\n\n1. **Preload intelligente**: quando l'utente passa il mouse su una voce di menu (o quando l'app è idle dopo il primo frame) chiama `loadLibrary()` in anticipo. Al click il chunk è già in cache e la navigazione è istantanea.\n2. **Gestione degli errori di rete**: dopo un nuovo deploy i vecchi chunk potrebbero non esistere più. Se `loadLibrary()` fallisce con un errore di caricamento, proponi un reload della pagina (`web.window.location.reload()`), perché l'`index.html` in cache punta a un build obsoleto.\n\nVerifica il risultato con `flutter build web --release` e controlla che in `build\u002Fweb` compaiano file `main.dart.js_*.part.js`: ogni file è un chunk che **non** viene scaricato all'avvio.","import 'package:flutter\u002Fmaterial.dart';\nimport 'package:go_router\u002Fgo_router.dart';\n\nimport 'features\u002Fhome\u002Fhome_page.dart';\nimport 'features\u002Freports\u002Freports_page.dart' deferred as reports;\nimport 'features\u002Fadmin\u002Fadmin_page.dart' deferred as admin;\nimport 'widgets\u002Fdeferred_builder.dart';\n\nfinal router = GoRouter(\n  routes: [\n    GoRoute(\n      path: '\u002F',\n      builder: (_, __) => const HomePage(),\n      routes: [\n        GoRoute(\n          path: 'reports',\n          builder: (_, __) => DeferredBuilder(\n            loader: reports.loadLibrary,\n            builder: (_) => reports.ReportsPage(),\n          ),\n        ),\n        GoRoute(\n          path: 'admin',\n          builder: (_, __) => DeferredBuilder(\n            loader: admin.loadLibrary,\n            builder: (_) => admin.AdminPage(),\n          ),\n        ),\n      ],\n    ),\n  ],\n);\n\n\u002F\u002F\u002F Preload dei moduli \"probabili\" quando la UI è stabile.\nclass ModulePrefetcher {\n  static bool _started = false;\n\n  static void scheduleAfterFirstFrame() {\n    if (_started) return;\n    _started = true;\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      Future\u003Cvoid>.delayed(const Duration(seconds: 2), () async {\n        \u002F\u002F Gli errori qui sono innocui: la rotta riproverà al bisogno.\n        await reports.loadLibrary().catchError((_) {});\n      });\n    });\n  }\n}\n\n\u002F\u002F Esempio di preload su hover di una voce di menu:\n\u002F\u002F MouseRegion(\n\u002F\u002F   onEnter: (_) => admin.loadLibrary().catchError((_) {}),\n\u002F\u002F   child: menuItem,\n\u002F\u002F )","In build\u002Fweb compaiono i file *.part.js; aprendo la rete del browser vedi che il chunk dei report viene scaricato solo alla navigazione (o in prefetch) e non all'avvio.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":40,"expected_result":63,"demo_url":42,"video_url":42},759,4,"Renderer e build WebAssembly: CanvasKit, skwasm e header COOP\u002FCOEP","Dopo il JavaScript dell'app, il secondo blocco pesante è il **renderer**. Con Flutter 3.22+ hai tre strade:\n\n- **CanvasKit (JS)**: resa fedele e stabile, ma scarica ~1,5 MB di WASM\u002FJS di Skia (in parte lazy e cacheable dal CDN).\n- **Compilazione Wasm (`--wasm`)**: l'app Dart stessa viene compilata in WebAssembly con renderer **skwasm**; prestazioni nettamente migliori su animazioni e scroll, startup più rapido sui browser moderni. Flutter genera comunque un fallback JS per i browser non compatibili.\n- Il vecchio renderer `html` è deprecato\u002Frimosso nelle versioni recenti: non progettare su quello.\n\nskwasm multi-thread richiede un contesto **cross-origin isolated**, quindi il server deve inviare gli header `Cross-Origin-Opener-Policy: same-origin` e `Cross-Origin-Embedder-Policy: credentialless` (o `require-corp`). Senza quegli header l'app funziona ma rinuncia ai thread, con un calo di fluidità.\n\nUn'altra leva è `--base-href` e l'hosting dei file CanvasKit in locale (`--pwa-strategy` e `FLUTTER_WEB_CANVASKIT_URL`) se non vuoi dipendere dal CDN di Google, ad esempio per policy CSP o GDPR.","# Build classica JS + CanvasKit\nflutter build web --release\n\n# Build WebAssembly (skwasm) con fallback JS automatico\nflutter build web --wasm --release --base-href \u002Fapp\u002F\n\n# Servire CanvasKit da dominio proprio invece che dal CDN\nflutter build web --release \\\n  --dart-define=FLUTTER_WEB_CANVASKIT_URL=\u002Fcanvaskit\u002F\n\n# --- nginx: header necessari per skwasm multi-thread ---\n# server {\n#   location \u002Fapp\u002F {\n#     add_header Cross-Origin-Opener-Policy   \"same-origin\"    always;\n#     add_header Cross-Origin-Embedder-Policy \"credentialless\" always;\n#\n#     # compressione: pre-comprimi in build e servi i .br\n#     brotli_static on;\n#     gzip_static   on;\n#\n#     # asset con hash: cache lunga\n#     location ~* \\.(js|wasm|woff2|png|json)$ {\n#       add_header Cache-Control \"public, max-age=31536000, immutable\";\n#     }\n#     # index.html: mai in cache\n#     location = \u002Fapp\u002Findex.html {\n#       add_header Cache-Control \"no-cache\";\n#     }\n#   }\n# }","L'app compilata con --wasm si avvia correttamente e, con gli header COOP\u002FCOEP attivi, `self.crossOriginIsolated` nella console del browser restituisce true.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":40,"expected_result":70,"demo_url":42,"video_url":42},760,5,"Snellire gli asset: icone, font e immagini","Il codice è solo una parte del payload. Le voci che più spesso gonfiano una build Flutter Web sono font e immagini.\n\n- **Icone**: il tree shaking è attivo di default in release e riduce `MaterialIcons` a poche decine di KB. Si **disattiva da solo** se costruisci `IconData` dinamicamente (`IconData(codePoint)`): in quel caso Flutter stampa un warning e include l'intero font. Mappa i codepoint su una `const` map di `IconData` per preservarlo.\n- **Font custom**: includi solo i pesi realmente usati e crea un **subset** dei glifi (latin + latin-ext) con `fonttools`. Si passa facilmente da 300 KB a 40 KB per peso. Dichiara i font in `pubspec.yaml` solo nei formati `.woff2`\u002F`.ttf` necessari.\n- **Immagini**: preferisci WebP\u002FAVIF, fornisci le risoluzioni con le varianti 2x\u002F3x e, per le immagini above-the-fold, aggiungi `\u003Clink rel=\"preload\">` nell'`index.html`.\n- **Package inutilizzati**: `dart pub deps` e l'analisi del passo 1 rivelano dipendenze trascinate per una sola funzione (classico: `intl` completo, o un SDK cloud usato solo in una feature → candidato perfetto al deferred loading).\n\nDopo ogni intervento, ripeti lo script di misura del passo 1: solo il confronto dei numeri ti dice se l'ottimizzazione è servita.","# 1) Subset di un font variabile su latin + latin-ext\npip install fonttools brotli\npyftsubset assets\u002Ffonts\u002FInter.ttf \\\n  --output-file=assets\u002Ffonts\u002FInter-subset.woff2 \\\n  --flavor=woff2 \\\n  --layout-features='kern,liga' \\\n  --unicodes=\"U+0000-00FF,U+0100-017F,U+2000-206F,U+20AC\"\n\n# 2) Verifica che il tree shaking delle icone sia attivo\nflutter build web --release 2>&1 | grep -i \"Font asset\"\n# Atteso: \"MaterialIcons-Regular.otf: ... 98% reduction\"\n\n# 3) Conversione immagini in WebP\nfor f in assets\u002Fimages\u002F*.png; do cwebp -q 82 \"$f\" -o \"${f%.png}.webp\"; done","Il log della build mostra la riduzione percentuale dei font icona e la cartella build\u002Fweb cala sensibilmente di peso rispetto alla baseline.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":77,"expected_result":78,"demo_url":42,"video_url":42},761,6,"Bootstrap personalizzato, splash e strategia di cache del service worker","Dalla versione 3.22 l'entrypoint web è `flutter_bootstrap.js`, inseribile in `web\u002Findex.html` con il token `{{flutter_bootstrap.js}}`. Puoi personalizzarlo per:\n\n- mostrare uno **splash HTML\u002FCSS** (zero JavaScript Flutter) rimosso quando il primo frame è pronto, eliminando la schermata bianca;\n- controllare la **strategia PWA** del service worker (`--pwa-strategy=offline-first` oppure `none` se gestisci la cache dal CDN);\n- scegliere il momento in cui avviare il motore (ad esempio dopo il caricamento di una configurazione runtime).\n\nSulla cache la regola d'oro è: `index.html` e `flutter_service_worker.js` **mai cachati** (`no-cache`), tutto il resto con hash e `immutable`. Il service worker generato da Flutter usa un manifest di hash: se l'HTML è in cache lunga, gli utenti restano bloccati su un build vecchio e i chunk differiti del passo 3 falliranno con 404.\n\nAggiungi infine un controllo di aggiornamento: quando il service worker installa una nuova versione, mostra una SnackBar \"Nuova versione disponibile — Ricarica\".","\u003C!DOCTYPE html>\n\u003Chtml>\n\u003Chead>\n  \u003Cbase href=\"$FLUTTER_BASE_HREF\">\n  \u003Cmeta charset=\"UTF-8\">\n  \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  \u003Ctitle>La mia app\u003C\u002Ftitle>\n  \u003Clink rel=\"manifest\" href=\"manifest.json\">\n  \u003Clink rel=\"preload\" href=\"assets\u002Ffonts\u002FInter-subset.woff2\" as=\"font\" type=\"font\u002Fwoff2\" crossorigin>\n  \u003Cstyle>\n    #splash {\n      position: fixed; inset: 0; display: grid; place-items: center;\n      background: #101418; color: #fff; font-family: system-ui, sans-serif;\n      transition: opacity .25s ease-out;\n    }\n    #splash.hidden { opacity: 0; pointer-events: none; }\n  \u003C\u002Fstyle>\n\u003C\u002Fhead>\n\u003Cbody>\n  \u003Cdiv id=\"splash\">\u003Cspan>Caricamento…\u003C\u002Fspan>\u003C\u002Fdiv>\n\n  \u003Cscript>\n    window.addEventListener('load', function () {\n      _flutter.loader.load({\n        serviceWorkerSettings: {\n          serviceWorkerVersion: {{flutter_service_worker_version}},\n        },\n        onEntrypointLoaded: async function (engineInitializer) {\n          const engine = await engineInitializer.initializeEngine({\n            \u002F\u002F renderer: 'canvaskit', \u002F\u002F forzatura opzionale\n          });\n          await engine.runApp();\n          const splash = document.getElementById('splash');\n          splash.classList.add('hidden');\n          setTimeout(() => splash.remove(), 300);\n        },\n      });\n    });\n  \u003C\u002Fscript>\n  \u003Cscript src=\"flutter_bootstrap.js\" async>\u003C\u002Fscript>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>","html","All'apertura l'utente vede subito lo splash HTML, che scompare esattamente quando compare il primo frame Flutter; ricaricando dopo un nuovo deploy l'app si aggiorna senza chunk 404.",{"id":80,"position":81,"title":82,"body":83,"code_snippet":84,"code_language":85,"expected_result":86,"demo_url":42,"video_url":42},762,7,"Bloccare le regressioni: performance budget in CI","Tutte le ottimizzazioni svaniscono al primo merge distratto che aggiunge un package pesante al bundle principale. Serve un **budget automatico**: la CI compila la web app, misura la dimensione Brotli di `main.dart.js` e fallisce se supera la soglia.\n\nAccorgimenti pratici:\n\n- fissa la soglia **appena sopra** il valore attuale (es. +5%), così ogni crescita significativa va giustificata;\n- misura anche il **numero di chunk differiti**: se scende a zero, qualcuno ha rotto il deferred loading con un import diretto;\n- aggiungi un job Lighthouse CI (`treosh\u002Flighthouse-ci-action`) con assert su *performance* e *First Contentful Paint*;\n- commenta il risultato sulla PR per rendere il costo visibile a chi rivede il codice.\n\nCon questo passo l'ottimizzazione smette di essere un intervento una tantum e diventa una proprietà verificata del progetto.","name: web-size-budget\n\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  budget:\n    runs-on: ubuntu-latest\n    env:\n      MAX_MAIN_KB: 1100   # budget Brotli per main.dart.js\n      MIN_CHUNKS: 2       # chunk differiti attesi\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: subosito\u002Fflutter-action@v2\n        with:\n          channel: stable\n          cache: true\n\n      - run: flutter pub get\n      - run: flutter build web --release\n\n      - name: Verifica budget\n        run: |\n          set -euo pipefail\n          size=$(brotli -q 11 -c build\u002Fweb\u002Fmain.dart.js | wc -c)\n          kb=$((size\u002F1024))\n          chunks=$(ls build\u002Fweb\u002Fmain.dart.js_*.part.js 2>\u002Fdev\u002Fnull | wc -l)\n\n          echo \"main.dart.js (brotli): ${kb} KB (budget ${MAX_MAIN_KB} KB)\"\n          echo \"chunk differiti: ${chunks} (minimo ${MIN_CHUNKS})\"\n\n          if [ \"$kb\" -gt \"$MAX_MAIN_KB\" ]; then\n            echo \"::error::Budget superato: ${kb} KB > ${MAX_MAIN_KB} KB\"\n            exit 1\n          fi\n          if [ \"$chunks\" -lt \"$MIN_CHUNKS\" ]; then\n            echo \"::error::Deferred loading rotto: solo ${chunks} chunk generati\"\n            exit 1\n          fi\n\n      - uses: actions\u002Fupload-artifact@v4\n        with:\n          name: web-build\n          path: build\u002Fweb","yaml","Una PR che aggiunge una dipendenza pesante al bundle principale o che rompe il deferred loading fa fallire la CI con un messaggio esplicito sul budget superato.",[],1792316928079]