[{"data":1,"prerenderedAt":80},["ShallowReactive",2],{"tutorial-performance-test-automatizzati-in-flutter-misurare-jank-e-frame-in-ci":3,"comments-tutorial-performance-test-automatizzati-in-flutter-misurare-jank-e-frame-in-ci":79},{"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},116,"Performance test automatizzati in Flutter: misurare jank e frame in CI","performance-test-automatizzati-in-flutter-misurare-jank-e-frame-in-ci","Impara a trasformare integration_test in una suite di benchmark: traccia la timeline, misura build e raster time, definisci soglie di jank e blocca le regressioni di performance direttamente in CI.","I widget test verificano la correttezza, ma non dicono nulla su **quanto scorre fluida** la tua UI. Una lista che perde frame o una transizione che va a scatti non fa fallire nessun test: se ne accorgono solo gli utenti.\n\nIn questo tutorial avanzato costruiamo una **suite di performance test automatizzati** basata su `integration_test` e `IntegrationTestWidgetsFlutterBinding.watchPerformance`. Impareremo a:\n\n- registrare la **timeline** di uno scenario reale (scroll di una lista pesante);\n- estrarre le metriche di `build` e `raster` time dal file `*.timeline_summary.json`;\n- definire **soglie di jank** e far fallire la pipeline in caso di regressione;\n- eseguire tutto su un device fisico\u002Femulatore in **profile mode** dentro GitHub Actions.\n\n> Prerequisito: conoscenza di base di `integration_test` e della pipeline di rendering (build → layout → paint → raster). I benchmark vanno **sempre** eseguiti in `--profile`, mai in debug: i tempi in debug sono inutilizzabili.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Faf3c3543-5f8d-48ec-b73f-859dc9263b0e.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1760689030974-37a6a5417d7b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTEyNjEyMDd8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Luke Miller","https:\u002F\u002Funsplash.com\u002F@bylukemiller","https:\u002F\u002Funsplash.com\u002Fphotos\u002Finterior-of-a-luxury-convertible-sports-car-with-orange-accents-JhmGFJ2U0GI","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=4nE-3jA4ons",{"youtube_id":17,"duration_seconds":18,"published_at":19},"4nE-3jA4ons",148,"2026-10-06T11:38:38+00:00","advanced",45,"3.x","published","2026-10-06T04:33:28+00:00","Performance test Flutter con integration_test","Misura jank, build e raster time in Flutter con integration_test: timeline, soglie di regressione e benchmark automatici in CI con GitHub Actions.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,50,57,64,72],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},777,"Configurare il progetto per i benchmark","Aggiungi `integration_test` alle dev dependencies e crea la cartella `integration_test\u002F`.\n\n```yaml\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  integration_test:\n    sdk: flutter\n```\n\nPer avere un benchmark **significativo** serve uno scenario rappresentativo e deterministico: niente dati random, niente immagini di rete (usa asset locali o placeholder), nessuna animazione infinita fuori controllo.\n\nCrea una schermata di prova con una lista lunga e costosa a sufficienza da stressare il raster thread (ombre, bordi arrotondati, gradienti).","\u002F\u002F lib\u002Fbenchmark\u002Fheavy_list_page.dart\nimport 'package:flutter\u002Fmaterial.dart';\n\nclass HeavyListPage extends StatelessWidget {\n  const HeavyListPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Heavy list')),\n      body: ListView.builder(\n        key: const Key('heavy_list'),\n        itemCount: 500,\n        itemBuilder: (context, i) => Container(\n          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n          height: 96,\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(16),\n            gradient: const LinearGradient(\n              colors: [Color(0xFF3F51B5), Color(0xFF00BCD4)],\n            ),\n            boxShadow: const [\n              BoxShadow(blurRadius: 12, offset: Offset(0, 6), color: Colors.black26),\n            ],\n          ),\n          alignment: Alignment.center,\n          child: Text('Elemento $i',\n              style: const TextStyle(color: Colors.white, fontSize: 18)),\n        ),\n      ),\n    );\n  }\n}","dart","Il progetto compila e la schermata `HeavyListPage` scorre una lista di 500 card con gradiente e ombra.",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":40,"expected_result":49,"demo_url":42,"video_url":42},778,2,"Scrivere il test che registra la timeline","Il punto chiave è `IntegrationTestWidgetsFlutterBinding.watchPerformance`: avvolge un blocco di codice, registra la timeline del tracing e produce un **summary** salvato su disco al termine della sessione (nella cartella `build\u002F` del progetto host).\n\nUsa `reportKey` per distinguere più scenari nello stesso run: ogni chiave genera un file `\u003CreportKey>.timeline_summary.json`.\n\nAttenzione a `await tester.pumpAndSettle()` **fuori** dal blocco misurato: vuoi misurare lo scroll, non il warm-up dei widget e la compilazione degli shader.","\u002F\u002F integration_test\u002Fscroll_perf_test.dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:integration_test\u002Fintegration_test.dart';\nimport 'package:myapp\u002Fbenchmark\u002Fheavy_list_page.dart';\n\nvoid main() {\n  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  testWidgets('scroll della lista pesante', (tester) async {\n    await tester.pumpWidget(const MaterialApp(home: HeavyListPage()));\n    await tester.pumpAndSettle();\n\n    \u002F\u002F Warm-up: primo fling non misurato (shader + cache immagini)\n    await tester.fling(find.byKey(const Key('heavy_list')), const Offset(0, -500), 1500);\n    await tester.pumpAndSettle();\n\n    await binding.watchPerformance(() async {\n      for (var i = 0; i \u003C 6; i++) {\n        await tester.fling(\n          find.byKey(const Key('heavy_list')),\n          const Offset(0, -400),\n          2500,\n        );\n        await tester.pumpAndSettle();\n        await tester.fling(\n          find.byKey(const Key('heavy_list')),\n          const Offset(0, 400),\n          2500,\n        );\n        await tester.pumpAndSettle();\n      }\n    }, reportKey: 'scroll_timeline');\n  });\n}","Eseguendo `flutter test integration_test\u002Fscroll_perf_test.dart --profile -d \u003Cdevice>` il test passa e viene generato `build\u002Fscroll_timeline.timeline_summary.json`.",{"id":51,"position":28,"title":52,"body":53,"code_snippet":54,"code_language":55,"expected_result":56,"demo_url":42,"video_url":42},779,"Leggere e interpretare le metriche del summary","Il file di summary contiene decine di chiavi. Quelle che contano davvero:\n\n| Chiave | Significato |\n|---|---|\n| `average_frame_build_time_millis` | tempo medio sul **UI thread** (build\u002Flayout\u002Fpaint) |\n| `90th_percentile_frame_build_time_millis` | coda alta: dove si nasconde il jank |\n| `average_frame_rasterizer_time_millis` | tempo medio sul **raster thread** (GPU) |\n| `missed_frame_build_budget_count` | frame oltre il budget sul UI thread |\n| `missed_frame_rasterizer_budget_count` | frame oltre il budget sul raster thread |\n| `frame_count` | numero totale di frame misurati |\n\nRegola pratica: a 60 Hz il budget è **16,67 ms**, a 120 Hz scende a **8,33 ms**. Se `frame_build_time` è alto il collo di bottiglia è nel tuo codice Dart (widget troppo grandi, `build` costosi, nessun `const`); se è alto il `rasterizer_time` il problema è grafico (`saveLayer`, opacità, blur, clip anti-aliasing, shader non precompilati).\n\nIl summary può anche essere esportato in formato leggibile insieme alla timeline completa, apribile su `chrome:\u002F\u002Ftracing` o su Perfetto per l'analisi manuale.","{\n  \"average_frame_build_time_millis\": 3.41,\n  \"90th_percentile_frame_build_time_millis\": 6.02,\n  \"worst_frame_build_time_millis\": 18.77,\n  \"average_frame_rasterizer_time_millis\": 5.88,\n  \"90th_percentile_frame_rasterizer_time_millis\": 11.4,\n  \"missed_frame_build_budget_count\": 2,\n  \"missed_frame_rasterizer_budget_count\": 7,\n  \"frame_count\": 420\n}","json","Sai leggere il summary e distinguere un problema di UI thread da uno di raster thread.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":40,"expected_result":63,"demo_url":42,"video_url":42},780,4,"Trasformare il summary in un test di regressione con soglie","Un numero su un file JSON non protegge da nulla: serve un **gate** che fallisca quando le metriche peggiorano. Crea un test Dart lato host (eseguito con `dart test` o `flutter test` sulla macchina, non sul device) che legge il summary e confronta i valori con un budget dichiarato.\n\nConsiglio: usa percentuali di frame persi anziché valori assoluti, così il test resta stabile su hardware diversi. Tieni le soglie **realistiche** e rivedile quando ottimizzi, altrimenti diventano rumore ignorato dal team.","\u002F\u002F test\u002Fperf_gate_test.dart\nimport 'dart:convert';\nimport 'dart:io';\nimport 'package:flutter_test\u002Fflutter_test.dart';\n\nclass PerfBudget {\n  const PerfBudget({\n    required this.maxP90Build,\n    required this.maxP90Raster,\n    required this.maxMissedFramesRatio,\n  });\n  final double maxP90Build;\n  final double maxP90Raster;\n  final double maxMissedFramesRatio;\n}\n\nvoid main() {\n  const budget = PerfBudget(\n    maxP90Build: 8.0,\n    maxP90Raster: 14.0,\n    maxMissedFramesRatio: 0.05,\n  );\n\n  test('scroll rispetta il performance budget', () {\n    final file = File('build\u002Fscroll_timeline.timeline_summary.json');\n    expect(file.existsSync(), isTrue,\n        reason: 'Esegui prima il benchmark in profile mode');\n\n    final s = jsonDecode(file.readAsStringSync()) as Map\u003CString, dynamic>;\n    double n(String k) => (s[k] as num).toDouble();\n\n    final frames = n('frame_count');\n    final missed = n('missed_frame_build_budget_count') +\n        n('missed_frame_rasterizer_budget_count');\n    final ratio = frames == 0 ? 1.0 : missed \u002F frames;\n\n    expect(n('90th_percentile_frame_build_time_millis'),\n        lessThan(budget.maxP90Build),\n        reason: 'Regressione sul UI thread');\n    expect(n('90th_percentile_frame_rasterizer_time_millis'),\n        lessThan(budget.maxP90Raster),\n        reason: 'Regressione sul raster thread');\n    expect(ratio, lessThan(budget.maxMissedFramesRatio),\n        reason: 'Troppi frame persi: ${(ratio * 100).toStringAsFixed(1)}%');\n  });\n}","Se una modifica peggiora lo scroll oltre il budget, il test fallisce indicando quale thread è in regressione.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":70,"expected_result":71,"demo_url":42,"video_url":42},781,5,"Automatizzare benchmark e gate in GitHub Actions","Il benchmark ha senso solo se gira **sempre sullo stesso hardware**: un emulatore con profilo fisso su un runner CI, oppure un device farm. Confrontare run su macchine diverse genera falsi positivi.\n\nLo schema della pipeline è:\n\n1. avvio emulatore Android con configurazione fissa;\n2. esecuzione del test `integration_test` in `--profile`;\n3. esecuzione del gate che legge il summary;\n4. upload dei file di timeline come artifact per l'analisi post-mortem.\n\nFai girare il job su `workflow_dispatch` e sui merge verso `main`, non su ogni commit: i benchmark sono lenti e rumorosi sulle PR frequenti.","# .github\u002Fworkflows\u002Fperf.yml\nname: Performance\non:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\njobs:\n  benchmark:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: subosito\u002Fflutter-action@v2\n        with: { channel: stable }\n      - run: flutter pub get\n      - name: Benchmark su emulatore\n        uses: reactivecircus\u002Fandroid-emulator-runner@v2\n        with:\n          api-level: 34\n          arch: x86_64\n          profile: pixel_6\n          disable-animations: false\n          script: flutter test integration_test\u002Fscroll_perf_test.dart --profile\n      - name: Performance gate\n        run: flutter test test\u002Fperf_gate_test.dart\n      - uses: actions\u002Fupload-artifact@v4\n        if: always()\n        with:\n          name: timeline\n          path: build\u002F*timeline*.json","yaml","Ad ogni merge su main la CI produce le metriche di scorrimento e blocca il merge successivo in caso di regressione, archiviando le timeline come artifact.",{"id":73,"position":74,"title":75,"body":76,"code_snippet":77,"code_language":40,"expected_result":78,"demo_url":42,"video_url":42},782,6,"Intervenire sulle regressioni: dal numero alla causa","Quando il gate fallisce, apri l'artifact `*.timeline.json` su [ui.perfetto.dev](https:\u002F\u002Fui.perfetto.dev) e cerca i frame fuori budget.\n\n**Se il problema è sul UI thread (build time):**\n- riduci la profondità del sottoalbero ricostruito: estrai widget e usa `const`;\n- sposta i calcoli pesanti fuori da `build` (memoizzazione, `compute`\u002Fisolate);\n- verifica che i `ValueNotifier`\u002Fprovider non facciano rebuild dell'intera pagina.\n\n**Se il problema è sul raster thread:**\n- elimina i `saveLayer` impliciti: `Opacity` → `AnimatedOpacity` solo dove serve, meglio ancora colori con alpha;\n- preferisci `ClipRRect` solo quando necessario, o usa `BoxDecoration(borderRadius:)`;\n- usa `RepaintBoundary` per isolare porzioni che si ridisegnano spesso;\n- con Impeller gli shader sono compilati in anticipo, ma blur e ombre restano costosi: riduci `blurRadius` o pre-renderizza.\n\nInfine aggiungi al repo un file `perf_budget.md` con le soglie e la data di revisione: il budget è un contratto di squadra, non un dettaglio del CI.","\u002F\u002F Isola il repaint della card animata dal resto della lista\nRepaintBoundary(\n  child: AnimatedBuilder(\n    animation: controller,\n    builder: (context, child) => Transform.scale(\n      scale: controller.value,\n      child: child, \u002F\u002F child costante: non viene ricostruito\n    ),\n    child: const HeavyCard(),\n  ),\n);","Dopo l'ottimizzazione il nuovo run di benchmark mostra p90 di build e raster sotto budget e il gate torna verde.",[],1792316927473]