[{"data":1,"prerenderedAt":86},["ShallowReactive",2],{"tutorial-pipeline-di-rendering-in-flutter-e-impeller-diagnosticare-e-ottimizzare-i-frame":3,"comments-tutorial-pipeline-di-rendering-in-flutter-e-impeller-diagnosticare-e-ottimizzare-i-frame":85},{"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},98,"Pipeline di rendering in Flutter e Impeller: diagnosticare e ottimizzare i frame","pipeline-di-rendering-in-flutter-e-impeller-diagnosticare-e-ottimizzare-i-frame","Guida avanzata alla pipeline di rendering di Flutter: thread UI e raster, frame budget, Impeller su Android e iOS, riduzione dei saveLayer, shader personalizzati e benchmark automatici dei frame.","Quando un'app Flutter \"scatta\", la causa non è quasi mai il codice Dart in senso generico: è un frame che ha sforato il budget in una delle fasi della **pipeline di rendering**. Capire come Flutter trasforma un albero di widget in pixel — e come Impeller ha cambiato le regole del gioco rispetto a Skia — è la differenza tra ottimizzare a caso e ottimizzare con il profiler in mano.\n\nIn questo tutorial avanzato vedremo:\n\n- le fasi della pipeline (**build → layout → paint → compositing → raster**) e i thread coinvolti;\n- come misurare in modo affidabile con `FrameTiming` e DevTools in modalità **profile**;\n- come verificare, attivare o disattivare **Impeller** su Android e iOS;\n- come ridurre il costo del raster eliminando i `saveLayer` inutili e usando bene i `RepaintBoundary`;\n- come gestire gli **shader personalizzati** (`FragmentProgram`) senza introdurre jank;\n- come trasformare le misure in un **benchmark automatico** ripetibile in CI.\n\n> Prerequisiti: buona dimestichezza con Flutter, widget custom e `CustomPainter`, uso di base di DevTools. Tutte le misure vanno fatte in `--profile` su **dispositivo fisico**: in debug i numeri non hanno alcun valore.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F1c9b0362-67d1-4725-a891-c3c76b7b9a80.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1725457878688-f3598513f162?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODk3MDU5NjF8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Steve A Johnson","https:\u002F\u002Funsplash.com\u002F@steve_j","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-multicolored-photo-of-a-tower-with-a-clock-on-it-YNCdBPG4_Ss","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=3AYG5iAsR60",{"youtube_id":17,"duration_seconds":18,"published_at":19},"3AYG5iAsR60",164,"2026-09-18T09:20:07+00:00","advanced",55,"3.x (3.27+ consigliato)","published","2026-09-18T04:32:42+00:00","Impeller e pipeline di rendering Flutter: guida avanzata","Come funziona la pipeline di rendering di Flutter: thread UI e raster, Impeller, saveLayer, RepaintBoundary, shader e benchmark dei frame in CI.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,50,57,64,71,78],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},654,"Mappare la pipeline: dalle fasi ai thread","Ogni frame Flutter attraversa una sequenza fissa, orchestrata dal `SchedulerBinding` a ogni segnale di **vsync**:\n\n1. **Animate \u002F build**: si eseguono i ticker delle animazioni e vengono ricostruiti i widget dirty (`Element.rebuild`).\n2. **Layout**: il `RenderObject` risolve i vincoli (`performLayout`), con il classico modello \"constraints down, sizes up\".\n3. **Paint**: ogni render object emette comandi di disegno su un `Canvas`, producendo un albero di `Layer`.\n4. **Compositing**: l'albero di layer viene convertito in una `Scene` e passata all'engine.\n5. **Raster**: l'engine (Impeller o Skia) traduce la scena in comandi GPU e li esegue.\n\nI punti 1–4 girano sul **thread UI** (dove vive l'isolate Dart principale), il punto 5 sul **thread raster**. Ci sono inoltre il **platform thread** (codice nativo, plugin, method channel) e il **IO thread** (decodifica\u002Fupload delle texture delle immagini).\n\nLa conseguenza pratica è fondamentale: un frame lento ha due possibili colpevoli, e la cura è diversa.\n\n- **UI thread lento** → troppi `build`\u002F`layout`, `setState` su sottoalberi enormi, JSON parsing sul main isolate, `ListView` senza `builder`. Si cura con memoization, `const`, split dei widget, `compute`\u002Fisolate.\n- **Raster thread lento** → troppi layer, `saveLayer`, clip con anti-aliasing, blur, ombre complesse, shader da compilare. Si cura riducendo il lavoro di disegno.\n\nPrima di scrivere una riga di ottimizzazione, aggiungi un listener ai `FrameTiming`: ti dice **quale** dei due tempi sfora.","import 'dart:developer' as developer;\nimport 'package:flutter\u002Fscheduler.dart';\n\n\u002F\u002F\u002F Registra i frame che sforano il budget, distinguendo UI e raster.\nclass FrameWatchdog {\n  FrameWatchdog({this.budget = const Duration(milliseconds: 16, microseconds: 600)});\n\n  final Duration budget;\n  bool _installed = false;\n\n  void start() {\n    if (_installed) return;\n    _installed = true;\n    SchedulerBinding.instance.addTimingsCallback(_onTimings);\n  }\n\n  void stop() {\n    SchedulerBinding.instance.removeTimingsCallback(_onTimings);\n    _installed = false;\n  }\n\n  void _onTimings(List\u003CFrameTiming> timings) {\n    for (final t in timings) {\n      final build = t.buildDuration; \u002F\u002F fase animate+build+layout+paint (thread UI)\n      final raster = t.rasterDuration; \u002F\u002F fase raster (thread GPU)\n      final total = t.totalSpan;\n      if (total \u003C= budget) continue;\n\n      final colpevole = build > raster ? 'UI' : 'RASTER';\n      developer.log(\n        'JANK [$colpevole] total=${total.inMicroseconds \u002F 1000}ms '\n        'ui=${build.inMicroseconds \u002F 1000}ms '\n        'raster=${raster.inMicroseconds \u002F 1000}ms',\n        name: 'perf',\n      );\n    }\n  }\n}\n\n\u002F\u002F main.dart (solo in profile: kProfileMode)\n\u002F\u002F void main() {\n\u002F\u002F   runApp(const MyApp());\n\u002F\u002F   if (kProfileMode) FrameWatchdog().start();\n\u002F\u002F }","dart","Lanciando `flutter run --profile` su dispositivo fisico, nel log compaiono righe `JANK [UI]` o `JANK [RASTER]` con i tempi delle due fasi: sai subito dove intervenire.",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":40,"expected_result":49,"demo_url":42,"video_url":42},655,2,"Profilare con DevTools: frame chart, timeline e flag di debug","Con il watchdog capisci *dove*; con DevTools capisci *perché*.\n\n**Setup corretto**\n\n```bash\nflutter run --profile -d \u003Cdevice-id>\n# per tracciare anche i build dei widget nella timeline\nflutter run --profile --trace-startup --verbose-system-logs\n```\n\nApri DevTools → tab **Performance**:\n\n- il **Frame Chart** mostra due barre per frame: blu = UI, verde-scuro = raster. La barra che supera la linea del budget è la fase colpevole.\n- attiva **Track Widget Builds** (Timeline Events) per vedere i `Widget Build` con i nomi delle classi;\n- la sezione **Rebuild Stats** ti dice quanti rebuild fa ogni widget in un intervallo: numeri a tre cifre su un widget \"statico\" sono un campanello d'allarme.\n\n**Flag runtime utilissimi** (da attivare a caldo da codice o dalla tab Inspector):\n\n- `debugRepaintRainbowEnabled`: colora il bordo dei layer che si ridipingono. Se lo sfondo lampeggia mentre scorri una lista, stai ridipingendo troppo.\n- `debugDisableClipLayers` \u002F `debugDisablePhysicalShapeLayers` \u002F `debugDisableOpacityLayers`: disattivano temporaneamente clip, ombre e opacity. Se il raster time crolla, hai trovato il costo.\n- `debugProfileBuildsEnabled` e `debugProfilePaintsEnabled`: emettono eventi di timeline per build e paint.\n- `--purge-persistent-cache` al lancio: utile quando indaghi la compilazione di shader.\n\nMisura sempre **lo stesso scenario** (stesso scroll, stessa animazione) prima e dopo: senza scenario riproducibile non esiste ottimizzazione dimostrabile.","import 'package:flutter\u002Ffoundation.dart';\nimport 'package:flutter\u002Frendering.dart';\n\n\u002F\u002F\u002F Attiva\u002Fdisattiva i flag di diagnostica a runtime da un pulsante di debug.\nvoid toggleRenderDiagnostics({\n  bool repaintRainbow = false,\n  bool disableOpacityLayers = false,\n  bool disableClipLayers = false,\n  bool profilePaints = false,\n}) {\n  assert(() {\n    debugRepaintRainbowEnabled = repaintRainbow;\n    debugDisableOpacityLayers = disableOpacityLayers;\n    debugDisableClipLayers = disableClipLayers;\n    debugProfilePaintsEnabled = profilePaints;\n    \u002F\u002F forza un repaint completo\n    RendererBinding.instance.renderViews.forEach((view) {\n      view.markNeedsPaint();\n    });\n    return true;\n  }());\n}\n\n\u002F\u002F\u002F Checkerboard dei layer offscreen: ogni area a scacchiera = un saveLayer.\n\u002F\u002F In MaterialApp:\n\u002F\u002F MaterialApp(\n\u002F\u002F   checkerboardOffscreenLayers: true,\n\u002F\u002F   checkerboardRasterCacheImages: true,\n\u002F\u002F   ...\n\u002F\u002F )","Sai leggere il frame chart, isolare la fase costosa e individuare visivamente i layer offscreen e i repaint superflui.",{"id":51,"position":28,"title":52,"body":53,"code_snippet":54,"code_language":55,"expected_result":56,"demo_url":42,"video_url":42},656,"Impeller: cos'è, come verificarlo e come gestirlo per piattaforma","**Impeller** è il motore di rendering che ha sostituito Skia come backend predefinito di Flutter su iOS (dalla 3.10) e su Android (dalla 3.27, con Vulkan e fallback OpenGL sui dispositivi non compatibili). La differenza architetturale chiave:\n\n- **Skia** compilava gli shader *a runtime*, la prima volta che serviva un certo effetto: da qui il famigerato **shader compilation jank** al primo scroll o alla prima transizione.\n- **Impeller** usa un set di shader **precompilati al build dell'engine**, con una pipeline pensata per tessellazione e antialiasing deterministici. Risultato: latenza del primo frame molto più prevedibile, nessun bisogno di warm-up SkSL.\n\n**Come verificare quale backend è in uso**\n\nNei log di avvio dell'app in profile\u002Fdebug Android compare una riga tipo `Using the Impeller rendering backend (Vulkan)` oppure `(OpenGLES)`. Se leggi `Impeller is disabled` o non vedi nulla, sei su Skia.\n\n**Come forzare la scelta**\n\n- Da CLI: `flutter run --enable-impeller` \u002F `flutter run --no-enable-impeller`.\n- Android, in modo persistente: meta-data nel manifest dentro `\u003Capplication>`.\n- iOS\u002FmacOS: chiave `FLTEnableImpeller` nell'`Info.plist`.\n\nUsa la disattivazione **solo come esperimento diagnostico**: se un glitch grafico sparisce con `--no-enable-impeller`, hai un bug di backend da segnalare, non una soluzione da spedire in produzione (su Android il supporto Skia è in via di dismissione).\n\n**Implicazioni pratiche di Impeller**\n\n- `--cache-sksl` e i file `flutter_01.sksl.json` non hanno più senso: quel warm-up serviva a Skia.\n- Il costo relativo di alcune operazioni cambia: blur e ombre sono gestiti diversamente, quindi **rimisura** le vecchie ottimizzazioni ereditate da app Skia.\n- Gli shader custom (`FragmentProgram`) sono compilati da `impellerc` in fase di build: la sintassi GLSL supportata è un sottoinsieme (niente loop non limitati, niente sampler dinamici).","\u003C!-- android\u002Fapp\u002Fsrc\u002Fmain\u002FAndroidManifest.xml -->\n\u003Capplication ...>\n\n    \u003C!-- true = forza Impeller, false = torna a Skia (solo per diagnosi) -->\n    \u003Cmeta-data\n        android:name=\"io.flutter.embedding.android.EnableImpeller\"\n        android:value=\"true\" \u002F>\n\n    \u003C!-- opzionale: forza il backend Vulkan anche su device in denylist -->\n    \u003Cmeta-data\n        android:name=\"io.flutter.embedding.android.ImpellerBackend\"\n        android:value=\"vulkan\" \u002F>\n\n\u003C\u002Fapplication>\n\n\u003C!-- ios\u002FRunner\u002FInfo.plist -->\n\u003C!--\n\u003Ckey>FLTEnableImpeller\u003C\u002Fkey>\n\u003Ctrue\u002F>\n-->","xml","Nei log di avvio leggi esplicitamente quale backend è attivo e puoi confrontare i tempi di raster dello stesso scenario tra Impeller e Skia.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":40,"expected_result":63,"demo_url":42,"video_url":42},657,4,"Ridurre il costo del raster: saveLayer, clip e RepaintBoundary","Quando il colpevole è il **raster thread**, l'obiettivo è ridurre il numero di **passaggi offscreen** e di layer ridipinti.\n\n**1. Elimina i `saveLayer` inutili.** Ogni `saveLayer` allocala una texture intermedia: è l'operazione più costosa che puoi chiedere alla GPU. Li generano `Opacity`, `ShaderMask`, `BackdropFilter`, `ColorFiltered`, `ClipPath`\u002F`ClipRRect` con `Clip.antiAliasWithSaveLayer`, e i `Transform` con opacità.\n\nSostituzioni tipiche:\n\n- `Opacity(opacity: .5, child: Image(...))` → `Image(..., opacity: AlwaysStoppedAnimation(.5))` oppure un colore con alpha.\n- `Opacity` su un `Container` colorato → colore con `withValues(alpha: ...)`.\n- `ClipRRect` su un'immagine → `DecorationImage` in un `BoxDecoration` con `borderRadius`.\n- `AnimatedOpacity` su sottoalberi grandi → `FadeTransition` (che usa un `OpacityLayer` ottimizzato) o animazione del colore.\n\n**2. Isola i repaint con `RepaintBoundary`.** Un `RepaintBoundary` crea un layer separato: se solo il suo contenuto cambia, il resto della scena non viene ridipinto. Va messo intorno a ciò che si anima *dentro* una scena statica (o viceversa). Attenzione: ogni boundary costa memoria, non spargerli a caso — misura.\n\n**3. Rispetta `shouldRepaint`.** In un `CustomPainter`, restituire `true` sempre significa ridipingere ogni frame. Confronta i campi effettivi.\n\n**4. Preferisci un painter unico a decine di widget.** Per grafici, griglie, badge sovrapposti: un `CustomPaint` con un `Path` unico batcha molto meglio di 200 `Container` decorati.\n\n**5. Attenzione alle ombre.** `BoxShadow` con `blurRadius` alto su liste lunghe è uno dei costi raster più sottovalutati: valuta `Material` con `elevation` (ombra ottimizzata) o un'immagine pre-renderizzata.","import 'package:flutter\u002Fmaterial.dart';\n\n\u002F\u002F\u002F Un badge animato dentro una card statica:\n\u002F\u002F\u002F il RepaintBoundary evita di ridipingere tutta la card a ogni frame.\nclass PulsingBadgeCard extends StatelessWidget {\n  const PulsingBadgeCard({super.key, required this.title, required this.progress});\n\n  final String title;\n  final Animation\u003Cdouble> progress;\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      \u002F\u002F ombra ottimizzata dal Material, invece di BoxShadow blurato\n      elevation: 2,\n      child: Padding(\n        padding: const EdgeInsets.all(16),\n        child: Row(\n          children: [\n            Expanded(child: Text(title)),\n            RepaintBoundary(\n              child: CustomPaint(\n                size: const Size.square(28),\n                painter: _RingPainter(progress),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _RingPainter extends CustomPainter {\n  _RingPainter(this.progress) : super(repaint: progress);\n\n  final Animation\u003Cdouble> progress;\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    \u002F\u002F Un solo Path, nessun saveLayer, nessun clip anti-aliased.\n    final rect = Offset.zero & size;\n    final paint = Paint()\n      ..style = PaintingStyle.stroke\n      ..strokeWidth = 4\n      ..strokeCap = StrokeCap.round\n      ..color = const Color(0xFF3F51B5);\n    canvas.drawArc(rect.deflate(2), -1.5708, 6.2832 * progress.value, false, paint);\n  }\n\n  \u002F\u002F Il repaint è già guidato da `repaint: progress`: qui confronto solo l'identità.\n  @override\n  bool shouldRepaint(_RingPainter old) => old.progress != progress;\n}","Con `checkerboardOffscreenLayers: true` le aree a scacchiera diminuiscono o scompaiono e il `rasterDuration` medio dello scenario cala in modo misurabile.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":40,"expected_result":70,"demo_url":42,"video_url":42},658,5,"Shader personalizzati con FragmentProgram senza introdurre jank","Con Impeller gli shader del framework sono precompilati, ma i **tuoi** shader GLSL vengono compilati da `impellerc` in fase di build e caricati a runtime: il caricamento è asincrono e va fatto **prima** che serva, mai dentro un `build` chiamato durante un'animazione.\n\n**1. Dichiara lo shader nel `pubspec.yaml`:**\n\n```yaml\nflutter:\n  shaders:\n    - shaders\u002Fgradient_wave.frag\n```\n\n**2. Scrivi il fragment shader** (sottoinsieme GLSL supportato: uniform scalari\u002Fvec, `sampler2D`, nessun ciclo illimitato):\n\n```glsl\n#version 460 core\n#include \u003Cflutter\u002Fruntime_effect.glsl>\n\nuniform vec2 uSize;\nuniform float uTime;\nout vec4 fragColor;\n\nvoid main() {\n  vec2 uv = FlutterFragCoord().xy \u002F uSize;\n  float wave = 0.5 + 0.5 * sin((uv.x * 6.0) + uTime);\n  fragColor = vec4(uv.y * 0.2, wave * 0.6, 0.9, 1.0);\n}\n```\n\n**3. Precarica il `FragmentProgram`** in fase di bootstrap (splash o route precedente), poi riusa **lo stesso** oggetto `FragmentShader` aggiornandone gli uniform: ricrearlo a ogni frame è un errore classico.\n\n**4. Regole d'oro**\n\n- gli `setFloat` vanno impostati **nell'ordine di dichiarazione** degli uniform;\n- non allocare `Paint`\u002F`FragmentShader` dentro `paint()` a ogni frame;\n- limita l'area coperta dallo shader: un fullscreen shader su 120 Hz è il modo più rapido per saturare la GPU;\n- verifica su dispositivi di fascia bassa con backend OpenGL, non solo su flagship Vulkan.","import 'dart:ui' as ui;\nimport 'package:flutter\u002Fmaterial.dart';\n\n\u002F\u002F\u002F Precarica una volta il programma e lo espone all'app.\nclass ShaderCache {\n  ShaderCache._();\n  static final instance = ShaderCache._();\n\n  final Map\u003CString, ui.FragmentProgram> _programs = {};\n\n  Future\u003Cvoid> preload(Iterable\u003CString> assets) async {\n    for (final asset in assets) {\n      _programs[asset] = await ui.FragmentProgram.fromAsset(asset);\n    }\n  }\n\n  ui.FragmentProgram operator [](String asset) {\n    final p = _programs[asset];\n    assert(p != null, 'Shader $asset non precaricato');\n    return p!;\n  }\n}\n\nclass WavePainter extends CustomPainter {\n  WavePainter({required this.time})\n      : _shader = ShaderCache.instance['shaders\u002Fgradient_wave.frag'].fragmentShader();\n\n  final double time;\n  final ui.FragmentShader _shader;\n  final Paint _paint = Paint();\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    \u002F\u002F Ordine identico alla dichiarazione degli uniform nel .frag\n    _shader\n      ..setFloat(0, size.width)\n      ..setFloat(1, size.height)\n      ..setFloat(2, time);\n    _paint.shader = _shader;\n    canvas.drawRect(Offset.zero & size, _paint);\n  }\n\n  @override\n  bool shouldRepaint(WavePainter old) => old.time != time;\n}\n\n\u002F\u002F main() {\n\u002F\u002F   WidgetsFlutterBinding.ensureInitialized();\n\u002F\u002F   ShaderCache.instance\n\u002F\u002F       .preload(['shaders\u002Fgradient_wave.frag'])\n\u002F\u002F       .then((_) => runApp(const MyApp()));\n\u002F\u002F }","Lo shader è disponibile al primo frame che lo usa: nessun buco iniziale nel frame chart e nessuna eccezione di asset non caricato.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":40,"expected_result":77,"demo_url":42,"video_url":42},659,6,"Alleggerire il thread UI: build, layout e lavoro fuori dall'isolate","Se il watchdog segnala `JANK [UI]`, la GPU non è il problema. Le cause ricorrenti, in ordine di frequenza reale:\n\n**1. Rebuild troppo ampi.** Un `setState` in cima all'albero ricostruisce tutto. Sposta lo stato più in basso, usa `ValueListenableBuilder`\u002F`AnimatedBuilder` con `child` passato a parte (il `child` non viene ricostruito), e marca `const` tutto ciò che è costante.\n\n**2. Layout costosi.** Evita `IntrinsicHeight`\u002F`IntrinsicWidth` nelle liste (forzano un doppio passaggio di layout), non abusare di `Expanded` annidati dentro `Column` dentro `SingleChildScrollView`, e usa `itemExtent`\u002F`prototypeItem` nelle `ListView` a elementi omogenei: consente di saltare il calcolo di layout di ogni riga.\n\n**3. Lavoro sincrono pesante nel frame.** Parsing JSON, decompressione, crittografia, ordinamenti su migliaia di elementi: fuori dal thread UI con `compute` o `Isolate.run`.\n\n**4. Animazioni guidate da `setState`.** Un `AnimationController` con `setState` nel listener ricostruisce l'intero widget 60–120 volte al secondo. Usa `AnimatedBuilder` mirato o, meglio, un `CustomPainter` con `repaint:` l'animazione: così salti build e layout e ridipingi solo il necessario.\n\n**5. Immagini non ridimensionate.** Una JPEG 4000×3000 mostrata in 120 px consuma banda su IO thread e memoria GPU: usa `cacheWidth`\u002F`cacheHeight` o `ResizeImage`.","import 'package:flutter\u002Fmaterial.dart';\n\n\u002F\u002F\u002F Animazione che NON ricostruisce il sottoalbero: il child è costruito una volta,\n\u002F\u002F\u002F l'AnimatedBuilder ricostruisce solo il Transform.\nclass EfficientSlideIn extends StatefulWidget {\n  const EfficientSlideIn({super.key, required this.child});\n  final Widget child;\n\n  @override\n  State\u003CEfficientSlideIn> createState() => _EfficientSlideInState();\n}\n\nclass _EfficientSlideInState extends State\u003CEfficientSlideIn>\n    with SingleTickerProviderStateMixin {\n  late final AnimationController _c = AnimationController(\n    vsync: this,\n    duration: const Duration(milliseconds: 350),\n  )..forward();\n\n  @override\n  void dispose() {\n    _c.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedBuilder(\n      animation: _c,\n      \u002F\u002F `child` viene costruito una sola volta e riusato in ogni tick\n      child: RepaintBoundary(child: widget.child),\n      builder: (context, child) => Transform.translate(\n        offset: Offset(0, (1 - _c.value) * 24),\n        child: Opacity(opacity: _c.value, child: child),\n      ),\n    );\n  }\n}\n\n\u002F\u002F\u002F Lista omogenea: itemExtent evita il layout riga per riga.\nclass FastList extends StatelessWidget {\n  const FastList({super.key, required this.items});\n  final List\u003CString> items;\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView.builder(\n      itemExtent: 72,\n      itemCount: items.length,\n      itemBuilder: (context, i) => _Row(label: items[i]),\n    );\n  }\n}\n\nclass _Row extends StatelessWidget {\n  const _Row({required this.label});\n  final String label;\n\n  @override\n  Widget build(BuildContext context) => ListTile(title: Text(label));\n}","Il `buildDuration` dello scenario scende e in Rebuild Stats i widget statici non compaiono più tra i più ricostruiti.",{"id":79,"position":80,"title":81,"body":82,"code_snippet":83,"code_language":40,"expected_result":84,"demo_url":42,"video_url":42},660,7,"Rendere le performance non regressive: benchmark automatico e checklist","Un'ottimizzazione senza test di regressione dura fino al prossimo sprint. Con `integration_test` puoi tracciare uno scenario, estrarre le metriche dei frame e salvarle come artefatto in CI.\n\n**Setup**\n\n```yaml\ndev_dependencies:\n  integration_test:\n    sdk: flutter\n  flutter_test:\n    sdk: flutter\n```\n\n**Esecuzione in profile**\n\n```bash\nflutter test integration_test\u002Fscroll_perf_test.dart --profile\n```\n\nIl `TimelineSummary` produce metriche come `average_frame_build_time_millis`, `90th_percentile_frame_build_time_millis`, `average_frame_rasterizer_time_millis` e `missed_frame_build_budget_count`. In CI confronta i percentili con una **baseline** salvata nel repo e fai fallire la build oltre una soglia (es. +15%): i valori medi mentono, i percentili no.\n\n**Checklist finale di rendering**\n\n- [ ] misure sempre in `--profile`, su device fisico, scenario riproducibile;\n- [ ] distingui UI vs raster prima di ottimizzare;\n- [ ] backend Impeller verificato nei log; nessun residuo di `--cache-sksl`;\n- [ ] zero `saveLayer` non necessari (`checkerboardOffscreenLayers` pulito);\n- [ ] `RepaintBoundary` solo dove misurabilmente utile;\n- [ ] `shouldRepaint` implementato correttamente in ogni `CustomPainter`;\n- [ ] shader custom precaricati, uniform aggiornati su istanze riusate;\n- [ ] immagini con `cacheWidth`\u002F`cacheHeight`; liste con `itemExtent` dove possibile;\n- [ ] lavoro CPU pesante fuori dal main isolate;\n- [ ] benchmark integration_test in CI con baseline e soglie sui percentili.","\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\u002Fmain.dart' as app;\n\nvoid main() {\n  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  testWidgets('scroll del feed entro il frame budget', (tester) async {\n    app.main();\n    await tester.pumpAndSettle();\n\n    await binding.traceAction(\n      () async {\n        final feed = find.byKey(const ValueKey('feed'));\n        for (var i = 0; i \u003C 6; i++) {\n          await tester.fling(feed, const Offset(0, -600), 4000);\n          await tester.pumpAndSettle();\n        }\n      },\n      reportKey: 'scroll_timeline',\n    );\n\n    final timeline = await binding.reportData!['scroll_timeline'];\n    final summary = TimelineSummary.summarize(Timeline.fromJson(timeline));\n\n    \u002F\u002F Artefatti per la CI\n    await summary.writeTimelineToFile('scroll_perf', pretty: true);\n    await summary.writeSummaryToFile('scroll_perf', pretty: true);\n\n    \u002F\u002F Guardrail: nessun percentile 90 sopra il budget di un frame a 60 Hz\n    expect(summary.computePercentileFrameBuildTimeMillis(90), lessThan(16.6));\n    expect(summary.computePercentileFrameRasterizerTimeMillis(90), lessThan(16.6));\n  });\n}","In `build\u002Fintegration_response_data\u002F` trovi `scroll_perf.timeline_summary.json` con i percentili dei tempi di build e raster; la pipeline CI fallisce se lo scenario regredisce oltre la soglia.",[],1792316930650]