[{"data":1,"prerenderedAt":87},["ShallowReactive",2],{"tutorial-dart-ffi-in-flutter-integrare-librerie-c-native-con-ffigen-memoria-e-isolate":3,"comments-tutorial-dart-ffi-in-flutter-integrare-librerie-c-native-con-ffigen-memoria-e-isolate":86},{"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":33},104,"Dart FFI in Flutter: integrare librerie C native con ffigen, memoria e isolate","dart-ffi-in-flutter-integrare-librerie-c-native-con-ffigen-memoria-e-isolate","Guida avanzata a dart:ffi in Flutter: template plugin_ffi, generazione dei binding con ffigen, gestione della memoria nativa, NativeFinalizer, chiamate bloccanti fuori dal thread UI e callback dal C con NativeCallable.","Quando una funzionalità richiede prestazioni estreme o riusa una libreria C\u002FC++ già esistente (codec, crittografia, computer vision, parser binari), i Platform Channel diventano un collo di bottiglia: ogni chiamata attraversa il ponte asincrono verso la piattaforma e serializza i dati.\n\n`dart:ffi` (Foreign Function Interface) permette invece di **chiamare direttamente il codice nativo dal thread Dart**, senza serializzazione e con overhead nell'ordine dei nanosecondi. In questo tutorial avanzato costruiremo un plugin FFI completo:\n\n- creazione del progetto con il template `plugin_ffi`;\n- generazione automatica dei binding con **ffigen**;\n- caricamento della `DynamicLibrary` su Android, iOS, macOS, Windows e Linux;\n- **gestione corretta della memoria nativa** con `package:ffi`, `calloc` e `NativeFinalizer`;\n- spostamento delle chiamate bloccanti fuori dal thread UI con `Isolate.run`;\n- callback asincrone dal C verso Dart con `NativeCallable.listener`.\n\n> **Prerequisiti**: Flutter 3.19+ (Dart 3.3+), toolchain nativa installata (NDK\u002FCMake per Android, Xcode per iOS\u002FmacOS), e confidenza con i puntatori in C. Il codice di esempio implementa una libreria di hashing\u002Felaborazione buffer, ma lo schema vale per qualsiasi libreria C.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Fb5665cb3-a546-43e3-b0a8-0fb7abad382e.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1615494937408-7f4e41bf45d9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTAyMjQzMDV8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Nathana Rebouças","https:\u002F\u002Funsplash.com\u002F@nathanareboucas","https:\u002F\u002Funsplash.com\u002Fphotos\u002Ftext-4adsYuQiaRw","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=QTHmJ6ERaPo",{"youtube_id":17,"duration_seconds":18,"published_at":19},"QTHmJ6ERaPo",149,"2026-09-24T09:40:10+00:00","advanced",55,"3.x","published","2026-09-24T04:31:46+00:00","Dart FFI in Flutter: librerie C con ffigen e isolate","Tutorial avanzato su dart:ffi in Flutter: plugin_ffi, binding con ffigen, memoria nativa, NativeFinalizer, isolate e NativeCallable per callback dal C.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,50,58,65,72,79],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},694,"Creare il plugin FFI e scrivere la libreria C","Il template `plugin_ffi` genera già la struttura corretta: sorgenti C in `src\u002F`, build Android via CMake, podspec per iOS\u002FmacOS e `CMakeLists.txt` per Windows\u002FLinux.\n\n```bash\nflutter create --template=plugin_ffi --platforms=android,ios,macos,windows,linux native_core\ncd native_core\n```\n\nStruttura rilevante:\n\n```\nnative_core\u002F\n├── src\u002F                 # sorgenti C\u002FC++ condivisi\n│   ├── native_core.c\n│   └── native_core.h\n├── android\u002FCMakeLists.txt\n├── ios\u002Fnative_core.podspec\n├── lib\u002F                 # API Dart pubblica\n└── ffigen.yaml\n```\n\nNell'header **è fondamentale** marcare le funzioni esportate con `FFI_PLUGIN_EXPORT`: su iOS\u002FmacOS il linker in release elimina i simboli non referenziati da codice nativo (tree-shaking), e senza l'attributo `visibility(\"default\")` + `__attribute__((used))` la `lookupFunction` fallirebbe a runtime.\n\nScriviamo un header con tre tipologie di funzioni: una veloce (sincrona), una bloccante (da eseguire in isolate) e una che restituisce memoria allocata dal nativo (che dovremo liberare).","\u002F\u002F src\u002Fnative_core.h\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n#if _WIN32\n#define FFI_PLUGIN_EXPORT __declspec(dllexport)\n#else\n#define FFI_PLUGIN_EXPORT __attribute__((visibility(\"default\"))) __attribute__((used))\n#endif\n\n\u002F\u002F Funzione veloce: si può chiamare direttamente dal thread UI.\nFFI_PLUGIN_EXPORT int32_t nc_checksum(const uint8_t *data, int32_t length);\n\n\u002F\u002F Funzione bloccante (simula lavoro pesante): va chiamata in un isolate.\nFFI_PLUGIN_EXPORT int64_t nc_heavy_transform(const uint8_t *data, int32_t length, int32_t rounds);\n\n\u002F\u002F Restituisce una stringa allocata con malloc: il chiamante deve liberarla.\nFFI_PLUGIN_EXPORT char *nc_version_string(void);\nFFI_PLUGIN_EXPORT void nc_free(void *ptr);\n\n#ifdef __cplusplus\n}\n#endif","c","Il progetto plugin `native_core` compila con `flutter build apk --debug` (o `flutter run` sull'example) e la libreria nativa viene inclusa nell'APK\u002Fframework.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":48,"expected_result":49,"demo_url":41,"video_url":41},695,2,"Generare i binding Dart con ffigen","Scrivere a mano `typedef` nativi e Dart è la principale fonte di bug (ABI mismatch → crash silenziosi). **ffigen** legge gli header C tramite libclang e genera una classe Dart type-safe.\n\nInstalla le dipendenze:\n\n```yaml\n# pubspec.yaml\ndependencies:\n  ffi: ^2.1.0\ndev_dependencies:\n  ffigen: ^13.0.0\n```\n\nSu macOS serve `brew install llvm`; su Linux `sudo apt-get install libclang-dev`.\n\nConfigura `ffigen.yaml` limitando rigorosamente cosa includere: senza `include` finisci per generare migliaia di simboli di `stdio.h`, `stdint.h` ecc.\n\nPoi esegui:\n\n```bash\ndart run ffigen --config ffigen.yaml\n```\n\nIl file generato **non va modificato a mano** e conviene rigenerarlo in CI per verificare che sia allineato agli header (`git diff --exit-code`).","# ffigen.yaml\nname: NativeCoreBindings\ndescription: Binding FFI generati per la libreria native_core.\noutput: 'lib\u002Fsrc\u002Fnative_core_bindings.dart'\nheaders:\n  entry-points:\n    - 'src\u002Fnative_core.h'\n  include-directives:\n    - '**native_core.h'   # solo i nostri simboli\nfunctions:\n  include:\n    - 'nc_.*'\ncomments:\n  style: any\n  length: full\npreamble: |\n  \u002F\u002F ignore_for_file: always_specify_types, camel_case_types, non_constant_identifier_names\n  \u002F\u002F GENERATO AUTOMATICAMENTE — non modificare a mano.","yaml","In `lib\u002Fsrc\u002Fnative_core_bindings.dart` trovi la classe `NativeCoreBindings` con i metodi `nc_checksum`, `nc_heavy_transform`, `nc_version_string` e `nc_free` tipizzati.",{"id":51,"position":52,"title":53,"body":54,"code_snippet":55,"code_language":56,"expected_result":57,"demo_url":41,"video_url":41},696,3,"Caricare la DynamicLibrary in modo cross-platform","Il caricamento della libreria cambia per piattaforma:\n\n| Piattaforma | Come caricare |\n|---|---|\n| Android | `DynamicLibrary.open('libnative_core.so')` |\n| iOS \u002F macOS (static o framework) | `DynamicLibrary.process()` oppure `DynamicLibrary.executable()` |\n| Windows | `DynamicLibrary.open('native_core.dll')` |\n| Linux | `DynamicLibrary.open('libnative_core.so')` |\n\nDue accortezze da progetto reale:\n\n1. **Carica una sola volta** e riusa l'istanza dei binding: `DynamicLibrary.open` mantiene un handle nel processo, ma aprire ripetutamente su isolate diversi è inutile e confonde il profiling.\n2. **Nei test Dart puri** (`dart test`) la libreria non è nel bundle: prevedi un percorso di fallback alla build locale, così puoi testare il codice FFI senza emulatore.\n\nCreiamo un piccolo loader con lazy initialization.","\u002F\u002F lib\u002Fsrc\u002Flibrary_loader.dart\nimport 'dart:ffi';\nimport 'dart:io' show Platform, File;\n\nimport 'native_core_bindings.dart';\n\nconst _libName = 'native_core';\n\nDynamicLibrary _open() {\n  if (Platform.isAndroid || Platform.isLinux) {\n    return DynamicLibrary.open('lib$_libName.so');\n  }\n  if (Platform.isIOS || Platform.isMacOS) {\n    \u002F\u002F Il plugin è linkato staticamente nell'app: i simboli sono nel processo.\n    return DynamicLibrary.process();\n  }\n  if (Platform.isWindows) {\n    return DynamicLibrary.open('$_libName.dll');\n  }\n  throw UnsupportedError('Piattaforma non supportata: ${Platform.operatingSystem}');\n}\n\n\u002F\u002F\u002F Caricata una sola volta per isolate, alla prima lettura.\nfinal NativeCoreBindings bindings = NativeCoreBindings(_resolve());\n\nDynamicLibrary _resolve() {\n  \u002F\u002F Fallback per i test da riga di comando su desktop.\n  const devPath = 'build\u002Fnative\u002Flibnative_core.so';\n  if (!Platform.isAndroid && File(devPath).existsSync()) {\n    return DynamicLibrary.open(devPath);\n  }\n  return _open();\n}","dart","Chiamando `bindings.nc_checksum(...)` da un'app di esempio non ottieni più `Invalid argument(s): Failed to lookup symbol`.",{"id":59,"position":60,"title":61,"body":62,"code_snippet":63,"code_language":56,"expected_result":64,"demo_url":41,"video_url":41},697,4,"Gestire la memoria nativa: puntatori, calloc e NativeFinalizer","Il garbage collector di Dart **non conosce** la memoria allocata dal C. Ogni `calloc.allocate` o `malloc` lato nativo deve avere un `calloc.free` \u002F `nc_free` corrispondente, altrimenti hai un memory leak invisibile ai DevTools.\n\nRegole operative:\n\n- usa sempre `try\u002Ffinally` attorno alle allocazioni temporanee;\n- per passare un `Uint8List` al C, alloca un buffer nativo e **copia** i byte con `asTypedList` (un `Uint8List` Dart può essere spostato dal GC, quindi non puoi prenderne l'indirizzo in modo stabile);\n- per risorse a vita lunga (handle, context, sessioni) usa `NativeFinalizer`: il runtime chiamerà la funzione di rilascio quando l'oggetto Dart wrapper diventa irraggiungibile. È una rete di sicurezza, **non** un sostituto di un metodo `dispose()` esplicito.\n\nAttenzione: la funzione passata a `NativeFinalizer` deve essere un `Pointer\u003CNativeFinalizerFunction>` con firma `void (*)(void*)`, esattamente come la nostra `nc_free`.","\u002F\u002F lib\u002Fsrc\u002Fnative_buffer.dart\nimport 'dart:ffi';\nimport 'dart:typed_data';\nimport 'package:ffi\u002Fffi.dart';\n\nimport 'library_loader.dart';\nimport 'native_core_bindings.dart';\n\n\u002F\u002F\u002F Chiamata veloce: copia i byte in memoria nativa, calcola e libera.\nint checksum(Uint8List data) {\n  final ptr = calloc\u003CUint8>(data.length);\n  try {\n    ptr.asTypedList(data.length).setAll(0, data);\n    return bindings.nc_checksum(ptr, data.length);\n  } finally {\n    calloc.free(ptr); \u002F\u002F obbligatorio anche in caso di eccezione\n  }\n}\n\n\u002F\u002F\u002F Stringa allocata dal C: va liberata con nc_free, non con calloc.free.\nString nativeVersion() {\n  final ptr = bindings.nc_version_string();\n  if (ptr == nullptr) throw StateError('nc_version_string ha restituito NULL');\n  try {\n    return ptr.cast\u003CUtf8>().toDartString();\n  } finally {\n    bindings.nc_free(ptr.cast\u003CVoid>());\n  }\n}\n\n\u002F\u002F\u002F Risorsa a vita lunga con doppia protezione: dispose() + NativeFinalizer.\nfinal _finalizer = NativeFinalizer(\n  nativeLibrary.lookup\u003CNativeFunction\u003CVoid Function(Pointer\u003CVoid>)>>('nc_free'),\n);\n\nclass NativeScratchBuffer implements Finalizable {\n  NativeScratchBuffer(this.size) : _ptr = calloc\u003CUint8>(size) {\n    _finalizer.attach(this, _ptr.cast\u003CVoid>(), detach: this, externalSize: size);\n  }\n\n  final int size;\n  Pointer\u003CUint8> _ptr;\n  bool _disposed = false;\n\n  Pointer\u003CUint8> get pointer {\n    if (_disposed) throw StateError('Buffer già liberato');\n    return _ptr;\n  }\n\n  void dispose() {\n    if (_disposed) return;\n    _disposed = true;\n    _finalizer.detach(this);\n    calloc.free(_ptr);\n    _ptr = nullptr;\n  }\n}","Eseguendo migliaia di chiamate in loop, la memoria RSS del processo resta stabile: nessuna crescita lineare dovuta a leak.",{"id":66,"position":67,"title":68,"body":69,"code_snippet":70,"code_language":56,"expected_result":71,"demo_url":41,"video_url":41},698,5,"Chiamate bloccanti fuori dal thread UI con gli isolate","Una chiamata FFI è **sincrona**: blocca il thread Dart finché il C non ritorna. Se la funzione nativa impiega 300 ms, l'UI perde ~18 frame. La soluzione è eseguirla in un isolate.\n\nDue approcci:\n\n- **`Isolate.run`** (Dart 3): perfetto per operazioni one-shot. Attenzione: i `Pointer` **non** si passano tra isolate come oggetti vivi — trasferisci `Uint8List` (copiati o via `TransferableTypedData`) e alloca la memoria nativa *dentro* l'isolate.\n- **Isolate long-running** con `ReceivePort`: evita il costo di spawn (~1-2 ms) quando le chiamate sono frequenti, ed è indispensabile se il nativo mantiene uno stato\u002Fsessione, perché quello stato deve essere usato sempre dallo stesso thread.\n\nRicorda anche che `DynamicLibrary` va caricata in ogni isolate (il nostro loader è lazy per isolate, quindi funziona già).\n\nPer grossi buffer usa `TransferableTypedData`: sposta i byte **senza copia** tra isolate.","\u002F\u002F lib\u002Fsrc\u002Fheavy_api.dart\nimport 'dart:isolate';\nimport 'dart:typed_data';\nimport 'dart:ffi';\nimport 'package:ffi\u002Fffi.dart';\n\nimport 'library_loader.dart';\n\n\u002F\u002F\u002F One-shot: la memoria nativa nasce e muore dentro l'isolate.\nFuture\u003Cint> heavyTransform(Uint8List data, {int rounds = 50}) {\n  final transfer = TransferableTypedData.fromList([data]);\n  return Isolate.run(() {\n    final bytes = transfer.materialize().asUint8List();\n    final ptr = calloc\u003CUint8>(bytes.length);\n    try {\n      ptr.asTypedList(bytes.length).setAll(0, bytes);\n      return bindings.nc_heavy_transform(ptr, bytes.length, rounds);\n    } finally {\n      calloc.free(ptr);\n    }\n  }, debugName: 'native_core.heavy');\n}\n\n\u002F\u002F\u002F Worker persistente: nessun costo di spawn per chiamata.\nclass NativeWorker {\n  NativeWorker._(this._toIsolate, this._responses, this._isolate);\n\n  final SendPort _toIsolate;\n  final Stream\u003CObject?> _responses;\n  final Isolate _isolate;\n\n  static Future\u003CNativeWorker> spawn() async {\n    final init = ReceivePort();\n    final isolate = await Isolate.spawn(_entryPoint, init.sendPort);\n    final broadcast = init.asBroadcastStream();\n    final sendPort = await broadcast.first as SendPort;\n    return NativeWorker._(sendPort, broadcast, isolate);\n  }\n\n  Future\u003Cint> transform(Uint8List data, int rounds) async {\n    final reply = ReceivePort();\n    _toIsolate.send([TransferableTypedData.fromList([data]), rounds, reply.sendPort]);\n    final result = await reply.first;\n    reply.close();\n    if (result is Object && result is! int) throw StateError('Errore nativo: $result');\n    return result as int;\n  }\n\n  void dispose() => _isolate.kill(priority: Isolate.immediate);\n\n  static void _entryPoint(SendPort initial) {\n    final commands = ReceivePort();\n    initial.send(commands.sendPort);\n    commands.listen((message) {\n      final args = message as List;\n      final bytes = (args[0] as TransferableTypedData).materialize().asUint8List();\n      final rounds = args[1] as int;\n      final reply = args[2] as SendPort;\n      final ptr = calloc\u003CUint8>(bytes.length);\n      try {\n        ptr.asTypedList(bytes.length).setAll(0, bytes);\n        reply.send(bindings.nc_heavy_transform(ptr, bytes.length, rounds));\n      } catch (e) {\n        reply.send(e.toString());\n      } finally {\n        calloc.free(ptr);\n      }\n    });\n  }\n}","Con il worker attivo, l'animazione di un `CircularProgressIndicator` resta fluida a 60\u002F120 fps anche durante trasformazioni da centinaia di millisecondi.",{"id":73,"position":74,"title":75,"body":76,"code_snippet":77,"code_language":56,"expected_result":78,"demo_url":41,"video_url":41},699,6,"Ricevere callback dal C in Dart con NativeCallable.listener","Molte librerie C notificano il progresso o gli eventi tramite puntatori a funzione. Con `NativeCallable` puoi passare una closure Dart al nativo:\n\n- **`NativeCallable.isolateLocal`**: la callback viene invocata in modo *sincrono* e **deve** arrivare dallo stesso thread che ha chiamato Dart. Usala solo per callback sincrone (es. comparatori).\n- **`NativeCallable.listener`**: la callback può essere invocata **da un thread nativo qualsiasi**; il messaggio viene accodato sull'event loop dell'isolate creatore ed eseguito in modo asincrono. È la scelta giusta per progressi, worker thread C e callback di completamento.\n\nRegole critiche:\n\n1. chiama sempre `close()` sul `NativeCallable`, altrimenti l'isolate resta vivo per sempre;\n2. nella callback **non fare assunzioni sulla validità dei puntatori**: se il nativo passa un buffer temporaneo, copialo subito (`asTypedList(...).sublist(0)`), perché con `listener` l'esecuzione avviene *dopo* il ritorno della funzione C;\n3. non lanciare eccezioni dalla callback: il nativo non sa gestirle.","\u002F\u002F lib\u002Fsrc\u002Fprogress_api.dart\nimport 'dart:async';\nimport 'dart:ffi';\n\nimport 'library_loader.dart';\n\ntypedef _ProgressNative = Void Function(Int32 percent, Pointer\u003CVoid> userData);\n\n\u002F\u002F\u002F Esegue un job nativo che riporta il progresso da un thread C.\n\u002F\u002F\u002F Firma C: void nc_run_job(int rounds, void (*cb)(int, void*), void* userData);\nStream\u003Cint> runJobWithProgress(int rounds) {\n  final controller = StreamController\u003Cint>();\n  late final NativeCallable\u003C_ProgressNative> callback;\n\n  void onProgress(int percent, Pointer\u003CVoid> _) {\n    if (controller.isClosed) return;\n    controller.add(percent);\n    if (percent >= 100) {\n      controller.close();\n      callback.close(); \u002F\u002F fondamentale: libera il thread di ascolto\n    }\n  }\n\n  callback = NativeCallable\u003C_ProgressNative>.listener(onProgress);\n\n  controller.onCancel = () {\n    callback.close();\n  };\n\n  \u002F\u002F La chiamata nativa fa partire un thread C e ritorna subito.\n  bindings.nc_run_job(rounds, callback.nativeFunction, nullptr);\n  return controller.stream;\n}","Un `StreamBuilder` collegato a `runJobWithProgress(200)` mostra la percentuale aggiornarsi in tempo reale mentre il codice C lavora su un thread separato.",{"id":80,"position":81,"title":82,"body":83,"code_snippet":84,"code_language":56,"expected_result":85,"demo_url":41,"video_url":41},700,7,"Build nativa, test e troubleshooting in produzione","Ultimo miglio: assicurarsi che la libreria venga compilata e inclusa su tutte le piattaforme, e che l'integrazione sia testata.\n\n**Android** — `android\u002FCMakeLists.txt` deve elencare i sorgenti e, se usi `Isolate.spawn` + `NativeCallable`, verifica di non avere problemi di 16 KB page size su Android 15: aggiungi i flag di link `-Wl,-z,max-page-size=16384`.\n\n**iOS\u002FmacOS** — nel `.podspec` mantieni `s.source_files = 'Classes\u002F**\u002F*', '..\u002Fsrc\u002F**\u002F*'` e assicurati che `DEFINES_MODULE = YES`. Se ottieni `Failed to lookup symbol` solo in **release**, quasi sempre manca `__attribute__((used))` nell'header.\n\n**Test** — i binding si testano con `dart test` su desktop compilando la libreria con `gcc\u002Fclang -shared`, il che rende la suite velocissima e indipendente dagli emulatori.\n\n**Diagnostica dei crash**: un `SIGSEGV` senza stack trace Dart indica quasi sempre un ABI mismatch (es. `Int32` invece di `Int64`, o struct passate per valore non allineate). Rigenera i binding con ffigen e confronta le firme.\n\n**Checklist finale**:\n\n- [ ] Ogni `allocate` ha il suo `free` in un `finally`.\n- [ ] Nessuna chiamata FFI > 8 ms sul thread UI.\n- [ ] Tutti i `NativeCallable` vengono chiusi.\n- [ ] I binding sono rigenerati e verificati in CI.\n- [ ] Testato su ABI `arm64-v8a`, `armeabi-v7a` (32 bit! attenzione a `IntPtr`) e `x86_64`.","\u002F\u002F test\u002Fnative_core_test.dart\nimport 'dart:typed_data';\nimport 'package:test\u002Ftest.dart';\nimport 'package:native_core\u002Fnative_core.dart';\n\nvoid main() {\n  test('checksum è deterministico e non perde memoria', () {\n    final data = Uint8List.fromList(List.generate(1024, (i) => i % 256));\n    final first = checksum(data);\n    for (var i = 0; i \u003C 10000; i++) {\n      expect(checksum(data), first);\n    }\n  });\n\n  test('la stringa nativa viene decodificata e liberata', () {\n    expect(nativeVersion(), matches(RegExp(r'^\\d+\\.\\d+\\.\\d+$')));\n  });\n\n  test('heavyTransform non blocca il chiamante', () async {\n    final data = Uint8List(1 \u003C\u003C 20);\n    var ticks = 0;\n    final timer = Stream.periodic(const Duration(milliseconds: 4)).listen((_) => ticks++);\n    await heavyTransform(data, rounds: 200);\n    await timer.cancel();\n    expect(ticks, greaterThan(5), reason: 'event loop rimasto responsivo');\n  });\n}","`dart test` passa su desktop, l'app di esempio gira su Android e iOS in release e il profiling DevTools non mostra jank durante le chiamate native pesanti.",[],1792316929523]