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 — выносите. Если длиннее и упирается в ожидание — вам нужен не изолят, а нормальная асинхронность.