[{"data":1,"prerenderedAt":85},["ShallowReactive",2],{"tutorial-pipeline-di-rendering-in-flutter-profilare-e-ottimizzare-con-repaintboundary-e-impeller":3,"comments-tutorial-pipeline-di-rendering-in-flutter-profilare-e-ottimizzare-con-repaintboundary-e-impeller":84},{"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},124,"Pipeline di rendering in Flutter: profilare e ottimizzare con RepaintBoundary e Impeller","pipeline-di-rendering-in-flutter-profilare-e-ottimizzare-con-repaintboundary-e-impeller","Guida avanzata alla pipeline di rendering Flutter: capire build, layout, paint e raster, isolare i repaint con RepaintBoundary, leggere il timeline e validare le ottimizzazioni con Impeller.","## Perché la pipeline conta\n\nQuando un'app Flutter \"scatta\", il problema raramente è il widget sbagliato: è il **lavoro svolto per frame** nelle fasi di build, layout, paint e raster.\n\nIn questo tutorial avanzato vedremo come:\n\n- leggere le fasi della pipeline (`build` → `layout` → `paint` → `composite` → `raster`);\n- misurare i frame con `SchedulerBinding` e i DevTools;\n- isolare i repaint costosi con `RepaintBoundary`;\n- ridurre il lavoro di raster con shader e ombre più economiche;\n- validare i risultati su Impeller.\n\n> Prerequisiti: Flutter 3.x, familiarità con `CustomPainter`, DevTools e build in **profile mode** su device fisico.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F0e9259f9-21ec-4888-912c-a48f4113e2c9.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1785204145393-caa8c6f9673b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTE5NTIzOTd8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Adhitya Sibikumar","https:\u002F\u002Funsplash.com\u002F@adhitya_2505","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-person-walks-past-a-yellow-k-pipe-wall--OBPhgYYTPQ","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=6WYkl9AkLl8",{"youtube_id":17,"duration_seconds":18,"published_at":19},"6WYkl9AkLl8",119,"2026-10-14T09:49:52+00:00","advanced",45,"3.x","published","2026-10-14T04:33:18+00:00","Rendering Flutter: RepaintBoundary e Impeller","Tutorial avanzato: profilare la pipeline di rendering Flutter, isolare i repaint con RepaintBoundary e ottimizzare i frame con Impeller e DevTools.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,50,56,63,70,77],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},828,"Preparare una scena volutamente costosa","Per misurare serve un caso realistico. Creiamo una schermata con un `CustomPainter` animato affiancato a una lista statica: senza accorgimenti, l'animazione forzerà il repaint dell'intero sottoalbero.\n\nEsegui sempre in **profile mode** su device fisico:\n\n```bash\nflutter run --profile -d \u003Cdevice-id>\n```\n\nIl debug mode include assert e controlli che falsano i tempi di build e raster.","class HeavyScene extends StatefulWidget {\n  const HeavyScene({super.key});\n  @override\n  State\u003CHeavyScene> createState() => _HeavySceneState();\n}\n\nclass _HeavySceneState extends State\u003CHeavyScene>\n    with SingleTickerProviderStateMixin {\n  late final AnimationController _c = AnimationController(\n    vsync: this,\n    duration: const Duration(seconds: 3),\n  )..repeat();\n\n  @override\n  void dispose() {\n    _c.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        SizedBox(\n          height: 220,\n          child: AnimatedBuilder(\n            animation: _c,\n            builder: (_, __) => CustomPaint(\n              painter: WavePainter(_c.value),\n              size: Size.infinite,\n            ),\n          ),\n        ),\n        Expanded(\n          child: ListView.builder(\n            itemCount: 200,\n            itemBuilder: (_, i) => ListTile(title: Text('Riga $i')),\n          ),\n        ),\n      ],\n    );\n  }\n}","dart","L'app gira in profile mode e mostra un'animazione continua sopra una lista scrollabile.",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":40,"expected_result":49,"demo_url":42,"video_url":42},829,2,"Misurare i frame: UI thread vs raster thread","Un frame ha due budget: il lavoro **Dart\u002FUI** (build, layout, paint) e il lavoro **raster** (esecuzione della display list sulla GPU). A 60 fps hai ~16,6 ms totali.\n\nAggiungi un timings callback per loggare i frame fuori budget: è il modo più rapido per capire *quale* thread è il collo di bottiglia prima ancora di aprire i DevTools.","void attachFrameMonitor() {\n  SchedulerBinding.instance.addTimingsCallback((List\u003CFrameTiming> timings) {\n    for (final t in timings) {\n      final ui = t.buildDuration.inMicroseconds \u002F 1000.0;\n      final raster = t.rasterDuration.inMicroseconds \u002F 1000.0;\n      final total = t.totalSpan.inMicroseconds \u002F 1000.0;\n      if (total > 16.6) {\n        debugPrint('JANK  ui=${ui.toStringAsFixed(1)}ms '\n            'raster=${raster.toStringAsFixed(1)}ms '\n            'total=${total.toStringAsFixed(1)}ms');\n      }\n    }\n  });\n}\n\n\u002F\u002F in main(), dopo WidgetsFlutterBinding.ensureInitialized()","In console compaiono righe JANK che distinguono chiaramente il tempo UI da quello raster.",{"id":51,"position":28,"title":52,"body":53,"code_snippet":54,"code_language":40,"expected_result":55,"demo_url":42,"video_url":42},830,"Capire chi si ripinta con i flag di debug","Prima di ottimizzare bisogna vedere i repaint. Flutter offre overlay di debug attivabili a runtime (solo in debug mode):\n\n- `debugRepaintRainbowEnabled`: colora i bordi dei layer a ogni repaint;\n- `debugProfileBuildsEnabled` \u002F `debugProfilePaintsEnabled`: aggiungono eventi al timeline;\n- `debugPrintMarkNeedsPaintStacks`: stampa lo stack di chi invalida il paint.\n\nSe il bordo attorno alla lista cambia colore mentre scorre solo l'animazione, stai ripingendo troppo.","import 'package:flutter\u002Frendering.dart';\n\nvoid enableRenderDebug() {\n  assert(() {\n    debugRepaintRainbowEnabled = true;\n    debugProfileBuildsEnabled = true;\n    debugProfilePaintsEnabled = true;\n    return true;\n  }());\n}","L'overlay arcobaleno evidenzia i sottoalberi che si ripingono a ogni frame, inclusa la lista che non dovrebbe farlo.",{"id":57,"position":58,"title":59,"body":60,"code_snippet":61,"code_language":40,"expected_result":62,"demo_url":42,"video_url":42},831,4,"Isolare i repaint con RepaintBoundary","`RepaintBoundary` crea un **layer separato**: il sottoalbero viene rasterizzato in una propria superficie e un suo repaint non propaga il lavoro ai fratelli.\n\nRegole pratiche:\n\n- inserisci un boundary attorno a ciò che si anima *frequentemente* (painter, progress, ticker);\n- oppure attorno a contenuti **statici e costosi** che non devono essere ridipinti;\n- non abusarne: ogni layer costa memoria GPU e una composizione in più.\n\nNei `CustomPaint` ricordati anche di `shouldRepaint` corretto e di `isComplex`\u002F`willChange` per guidare la cache del raster.","class WavePainter extends CustomPainter {\n  WavePainter(this.t);\n  final double t;\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    final paint = Paint()..color = const Color(0xFF2962FF);\n    final path = Path()..moveTo(0, size.height);\n    for (double x = 0; x \u003C= size.width; x += 4) {\n      final y = size.height \u002F 2 +\n          30 * math.sin((x \u002F size.width * 2 * math.pi) + t * 2 * math.pi);\n      path.lineTo(x, y);\n    }\n    path..lineTo(size.width, size.height)..close();\n    canvas.drawPath(path, paint);\n  }\n\n  @override\n  bool shouldRepaint(WavePainter old) => old.t != t;\n}\n\n\u002F\u002F Uso: animazione isolata + lista isolata\nColumn(\n  children: [\n    RepaintBoundary(\n      child: SizedBox(\n        height: 220,\n        child: AnimatedBuilder(\n          animation: _c,\n          builder: (_, __) => CustomPaint(\n            painter: WavePainter(_c.value),\n            isComplex: true,\n            willChange: true,\n            size: Size.infinite,\n          ),\n        ),\n      ),\n    ),\n    const Expanded(child: RepaintBoundary(child: StaticList())),\n  ],\n)","Con il rainbow attivo solo l'area dell'animazione lampeggia; il tempo UI per frame cala in modo misurabile.",{"id":64,"position":65,"title":66,"body":67,"code_snippet":68,"code_language":40,"expected_result":69,"demo_url":42,"video_url":42},832,5,"Ridurre il costo del raster: ombre, opacità e saveLayer","Se il collo di bottiglia è il **raster thread**, il problema è nella display list, non nei widget. Le cause più comuni:\n\n- `Opacity` animata su sottoalberi grandi → usa `AnimatedOpacity` solo su foglie, o `FadeTransition` con `RepaintBoundary`;\n- `ClipPath` + `BackdropFilter` sovrapposti → ogni filtro forza un `saveLayer`;\n- ombre complesse: `BoxShadow` con blur alto su molte card.\n\nUn buon compromesso è pre-renderizzare contenuti statici in immagine con `RepaintBoundary.toImage()` quando il contenuto cambia di rado.","\u002F\u002F Male: opacità animata su un sottoalbero ampio\nOpacity(opacity: _c.value, child: const ExpensiveSubtree());\n\n\u002F\u002F Meglio: transizione su layer dedicato\nRepaintBoundary(\n  child: FadeTransition(\n    opacity: _c,\n    child: const RepaintBoundary(child: ExpensiveSubtree()),\n  ),\n);\n\n\u002F\u002F Snapshot di contenuti statici costosi\nFuture\u003Cui.Image> snapshot(GlobalKey key, double dpr) async {\n  final boundary =\n      key.currentContext!.findRenderObject()! as RenderRepaintBoundary;\n  return boundary.toImage(pixelRatio: dpr);\n}","Il valore rasterDuration nei log scende sotto il budget anche durante le transizioni.",{"id":71,"position":72,"title":73,"body":74,"code_snippet":75,"code_language":40,"expected_result":76,"demo_url":42,"video_url":42},833,6,"Validare su Impeller e misurare in CI","Impeller è il motore di rendering predefinito su iOS e Android recenti: **precompila gli shader**, eliminando il jank del primo frame tipico di Skia.\n\nVerifiche utili:\n\n```bash\n# forzare il motore per confronto (Android)\nflutter run --profile --enable-impeller\nflutter run --profile --no-enable-impeller\n```\n\nPer rendere l'ottimizzazione non regressiva, raccogli le metriche con `integration_test` e `traceAction`, esportando il summary in CI.","\u002F\u002F integration_test\u002Fperf_test.dart\nvoid main() {\n  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  testWidgets('scroll senza jank', (tester) async {\n    await tester.pumpWidget(const MyApp());\n    await tester.pumpAndSettle();\n\n    await binding.traceAction(() async {\n      for (var i = 0; i \u003C 5; i++) {\n        await tester.fling(\n            find.byType(ListView), const Offset(0, -400), 3000);\n        await tester.pumpAndSettle();\n      }\n    }, reportKey: 'scrolling_summary');\n  });\n}","Il summary riporta average_frame_build_time_millis e average_frame_rasterizer_time_millis, confrontabili tra build per bloccare le regressioni.",{"id":78,"position":79,"title":80,"body":81,"code_snippet":82,"code_language":40,"expected_result":83,"demo_url":42,"video_url":42},834,7,"Checklist finale e anti-pattern da evitare","Prima di chiudere il lavoro, verifica:\n\n1. **Misura sempre in profile mode** su device reale, mai in debug o simulatore.\n2. **Un boundary per motivo**, non per abitudine: controlla in DevTools > Layers se i layer sono cresciuti troppo.\n3. **`const` ovunque possibile**: riduce il lavoro di build prima ancora del paint.\n4. **`shouldRepaint` e `shouldRebuildSemantics`** corretti nei painter custom.\n5. **Evita `MediaQuery.of(context)` in alto nell'albero**: invalida build inutili a ogni rotazione o apertura tastiera.\n6. **Shader warm-up**: con Impeller non serve più il bundle di shader di Skia, rimuovi vecchi `--bundle-sksl-path` dalle pipeline.\n\nAnti-pattern ricorrenti: `Opacity` al posto di colori con alpha, `ListView` senza `itemExtent` o `prototypeItem` su righe omogenee, immagini di rete non ridimensionate (`cacheWidth`).","\u002F\u002F Liste omogenee: evita il layout ripetuto di ogni figlio\nListView.builder(\n  prototypeItem: const ListTile(title: Text('prototipo')),\n  itemCount: items.length,\n  itemBuilder: (_, i) => ListTile(title: Text(items[i])),\n);\n\n\u002F\u002F Immagini: decodifica alla dimensione reale di schermo\nImage.network(url, cacheWidth: 320);","L'app mantiene frame stabili sotto i 16,6 ms durante scroll e animazioni, con metriche riproducibili in CI.",[],1792316904070]