flutterprincess

Isolate.run и когда он не спасает

В Dart один поток исполнения на изолят, и весь UI живёт в главном. Значит, любая синхронная работа дольше нескольких миллисекунд — это пропущенные кадры. Отсюда стандартный рецепт: вынести тяжёлое в отдельный изолят. Рецепт правильный, но у него есть цена, которую редко считают.

Базовый случай

С Isolate.run это одна строка:

final report = await Isolate.run(() => buildReport(rawRows));

Функция уезжает в свежий изолят, результат возвращается, изолят закрывается. Для разовой тяжёлой операции — разбор большого JSON, генерация PDF, обсчёт статистики — этого достаточно и ничего лучше не нужно.

Цена, которую платят

Изоляты не делят память. Всё, что вы передаёте внутрь и получаете обратно, копируется. Для списка из ста тысяч объектов копирование может занять больше, чем сама обработка — и вы получите тот же джанк, только теперь ещё и с накладными расходами на запуск изолята.

Простая проверка: замерьте отдельно время работы функции и время всего вызова Isolate.run. Если разница большая, вы упёрлись в копирование.

final sw = Stopwatch()..start();
final report = await Isolate.run(() => buildReport(rawRows));
print('всего: ${sw.elapsedMilliseconds} мс');

Лечится это тем, что через границу изолята гоняют не структуры, а байты. TransferableTypedData передаёт буфер без копирования — владение просто переходит другой стороне:

final transferable = TransferableTypedData.fromList([bytes]);
final parsed = await Isolate.run(() {
  final data = transferable.materialize().asUint8List();
  return decodeAndAggregate(data);
});

Обратите внимание: после materialize исходная сторона к данным обратиться уже не может. Это не ограничение реализации, а весь смысл — именно поэтому копии не происходит.

Когда изолят не нужен вовсе

Если операция уже асинхронная и упирается не в процессор, а в ожидание — сеть, диск, база — изолят не даст ничего. await на сетевом запросе главный поток не блокирует, ему нечего разгружать.

Отдельно про декодирование картинок: этим движок и так занимается вне главного потока. Оборачивать decodeImageFromList в изолят вручную не надо.

Долгоживущий изолят

Isolate.run поднимает изолят на каждый вызов, а это порядка единиц миллисекунд плюс прогрев. Если вы дёргаете его в цикле или на каждый ввод пользователя, поднимите один изолят с портами и держите его.

Писать это руками поверх ReceivePort и SendPort довольно муторно и легко ошибиться в закрытии портов, поэтому на практике я обычно беру готовое решение вроде пула воркеров. Своё имеет смысл писать, только если протокол общения нетривиальный.

Правило, которым я пользуюсь: если операция короче примерно 16 мс — оставляйте в главном изоляте. Если длиннее и упирается в CPU — выносите. Если длиннее и упирается в ожидание — вам нужен не изолят, а нормальная асинхронность.