ATL-2026-011benchmarkexperimental

JSON вынесли в isolate. А какую проблему решили?

Разбор JSON в отдельном isolate может вернуть данные позже и одновременно сократить паузу анимации. Эксперимент Flutter на M1 показывает, почему критерий оптимизации нужно определить до выбора API.

Опубликовано
25 сентября 2026 г.
Проверено
23 сентября 2026 г.

JSON вынесли в isolate. А какую проблему решили?

Индикатор загрузки останавливается, пока приложение разбирает большой ответ сервера. Разработчик переносит разбор в отдельный isolate, анимация перестаёт замирать. Затем замеряет время выполнения и обнаруживает, что результат теперь приходит позже.

Изменение легко признать неудачным: операция стала медленнее. Можно защитить его противоположным аргументом: индикатор снова вращается, значит, приложение оптимизировано. Обе оценки пропускают существенную часть задачи. Первая не учитывает, что происходило с интерфейсом во время обработки. Вторая ничего не говорит о том, сколько пользователь теперь ждёт нужные данные.

Так можно откатить полезное исправление по результатам аккуратного benchmark. Или оставить бесполезное, потому что оно убедительно выглядит на экране.

До выбора способа исполнения приходится ответить на менее технический вопрос: какое поведение требуется исправить? Уменьшить ожидание результата, убрать паузу анимации, сохранить возможность ввода во время загрузки? Эти требования могут относиться к одной операции, но проверяются по-разному. Слово «производительность» позволяет слишком долго делать вид, что речь идёт об одном числе.

Быстрее закончить — не единственное требование

В Flutter синхронный разбор JSON и создание прикладных объектов могут занимать тот же основной isolate, которому нужно обслуживать интерфейс. Пока выполняется эта работа, он не может перейти к следующему событию своего цикла. async у окружающего метода само по себе эту работу никуда не переносит.

Для одновременного выполнения нужен отдельный контекст со своим циклом событий — другой isolate. Ему передаётся задание, вызывающая сторона получает результат позже. У такого разделения есть цена: запуск, обмен сообщениями, доставка объектов. Сэкономить время ожидания оно не обещает.

Здесь полезен конкретный результат эксперимента ArkTelos Lab. Одна и та же заранее подготовленная строка превращалась в полный список объектов тремя способами: на основном isolate, в новом isolate на каждый запрос и в постоянно работающем isolate. Последний далее называется worker. В стенде он принимает задания последовательно и остаётся готовым к следующему после завершения предыдущего.

На MacBook Air M1 приложение Flutter в режиме profile обрабатывало JSON из 100 тысяч записей, около 14,6 МБ. На экране вращался небольшой квадрат, а после обработки обновлялось только число записей. Для каждой стратегии выполнено 50 запросов в пяти десятисекундных окнах.

Способ выполнения Медиана времени до полного списка Медиана длинного кадрового промежутка около запроса
Основной isolate 210,1 мс 219,2 мс
Новый isolate 273,1 мс 20,4 мс
Постоянный worker 339,4 мс 23,2 мс

Во второй числовой колонке для каждого запроса взят самый длинный промежуток между кадровыми callback от начала обработки до её завершения плюс два бюджета кадра. Кадровый callback здесь — вызов, по которому приложение отмечает очередной кадр; измеренный промежуток не равен точному числу кадров, пропущенных дисплеем. Экран работал на 60 Гц, то есть бюджет одного обновления составлял около 16,67 мс.

Новый isolate вернул список примерно на 63 мс позже, но характерный длинный промежуток сократился с 219 до 20 мс. Если целью было сократить остановку анимации во время разбора, это полезное изменение с измеренной ценой. Если единственным требованием было раньше получить весь список, преимущество по этому критерию осталось у основного isolate.

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

Если экран на всё время загрузки всё равно запрещает любые действия, перенос может улучшить только движение индикатора. Это тоже полезная обратная связь, но задача сохранения взаимодействия с приложением ещё не решена. Для неё требуется проверить доступные действия, а не только наблюдать анимацию.

Убедительная метрика может ответить на другой вопрос

Для основного isolate из той же серии можно выбрать ещё одно число: p95 всех промежутков между кадровыми callback — около 17,2 мс. Этот показатель задаёт границу, в которую укладываются не менее 95% записанных промежутков. По такой сводке трудно заподозрить регулярные паузы за 200 мс.

Ошибки в расчёте для этого не требуется. Запрос выполнялся раз в секунду, между запросами приложение успевало обслужить много обычных кадров. Редкие длинные промежутки оказались за пределами выбранного процентиля. А p95 длительности самого построения кадра составил всего 0,47 мс: короткая работа после паузы не описывает ожидание до её начала.

У метрики изменился смысл, когда её оторвали от действия пользователя. «Большинство кадров укладывается в бюджет» и «обработка ответа не создаёт длинную паузу» — разные утверждения. Первое вполне может быть верным при нарушении второго.

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

Это не основание заменить все показатели максимумом: единичный максимум тоже может отражать постороннее событие. В контроле без JSON встретился кадровый промежуток 55,8 мс. Нужны повторяемость, привязка к операции и контрольное поведение. Общая сводка полезна, пока известно, какое наблюдение она сохраняет, а какое скрывает.

Где должна закончиться вынесенная работа

После решения перенести обработку возникает соблазн провести границу по названию функции: дорогой jsonDecode выполняется отдельно, всё остальное остаётся как было. Но экрану обычно нужен не результат синтаксического разбора. Ему нужны объекты, с которыми можно работать.

Между этими точками могут находиться создание моделей, проверка полей, фильтрация, сортировка. Если перенести только первый шаг, остальная синхронная работа по-прежнему достанется основному isolate. Насколько она тяжела, нужно измерить; сам факт использования отдельного isolate этого вопроса не закрывает.

В стенде граница проведена по готовому результату. parseCatalog разбирает строку и полностью создаёт List<CatalogItem>, включая вложенные списки тегов. CatalogItem — обычная запись с идентификатором, названием, целочисленной ценой, признаком доступности и тегами. Ленивого преобразования, которое продолжит создавать записи после возврата, здесь нет.

Future<List<CatalogItem>> parseFresh(String input) =>
    Isolate.run(() => parseCatalog(input), debugName: 'catalog-fresh');

Этот фрагмент взят из стенда; определения класса и парсера доступны в полном исходнике. Помощник расположен вне объекта экрана и получает только необходимый вход. Это существенно: замыкание может неявно захватить лишнее состояние, а передаваемые объекты должны допускать обмен между isolates. Документация Isolate.run.

Для конкретного экрана законченный результат может быть другим. Например, уже отфильтрованная и отсортированная подборка. Тогда стоит рассматривать подготовку этой подборки как целый этап, а не возвращать промежуточные данные только потому, что закончился jsonDecode.

Однако переносить вслед за данными весь объект экрана не требуется. Представление остаётся на своей стороне, а вычислению передаётся необходимая информация. Полезная граница определяется тем, какой результат нужен дальше и кто может его подготовить, не зная о виджетах.

Вариант с фильтрацией в этом эксперименте не измерялся. Это следующее проектное решение, а не ещё один выигрыш, якобы подтверждённый таблицей. Даже после доставки готовых объектов построение тяжёлого интерфейса способно создать собственную паузу: работа с данными не отменяет работу представления.

Что именно сохраняет постоянный worker

Постоянный worker выглядит естественным продолжением оптимизации. Вместо создания isolate на каждый запрос можно запустить его один раз. Но в таблице такой вариант вернул большой список позже остальных. Простое объяснение «запуск дорогой, повторное использование выгоднее» оказалось недостаточным для полного результата.

У способов возврата есть существенное различие. Isolate.run завершает созданный isolate и передаёт результат через Isolate.exit без копирования. Продолжающий работать worker отправляет сообщение через SendPort.send: неизменяемые объекты, например строки, могут разделяться, остальные части передаваемой структуры копируются. Большой список экземпляров с вложенными списками требует учитывать эту работу. Isolate.run, SendPort.send.

Измерения не позволяют приписать весь разрыв копированию: отдельно его стоимость, сборка мусора и планирование потоков не выделялись. Но само различие механизмов уже показывает, чего не хватает аргументу про экономию запуска. Сохранить обработчик между запросами и дёшево доставить результат — две отдельные задачи.

Более интересный вопрос возникает раньше: зачем полный каталог каждый раз нужен вызывающей стороне?

Допустим, экран показывает небольшую подборку, а остальные записи нужны для последующих поисков. Каталог можно оставить рядом с обработчиком и передавать ему условия поиска, возвращая только выбранные записи. Объём обмена изменится; выигрыш по времени для такого варианта в стенде не измерялся.

Такой worker получает новую ответственность: хранить данные и выполнять операции над ними. Придётся определить, когда каталог обновляется, какой версии соответствует ответ, кому принадлежат изменения и когда хранилище завершается. Если данные меняются на обеих сторонах без ясного правила, экономия на передаче может обернуться отдельной задачей согласования.

Для однократной загрузки это вполне может оказаться ненужной сложностью. Для повторных запросов к одной большой структуре — осмысленной архитектурой. Выбор зависит уже от того, кому нужны данные и как долго они живут. Количество строк в реализации worker этого не объясняет.

И здесь особенно легко получить эффектный, но нечестный benchmark. Основной isolate возвращает весь каталог, а worker — число записей или маленькую подборку. Во втором случае передаётся меньше данных и выполняется другой договор с вызывающим кодом. Такой дизайн можно сравнивать по конечной задаче экрана, но нельзя выдавать его результат за ускорение прежней операции с тем же выходом.

Перенос вычисления не решает, нужна ли ещё его работа

После освобождения основного isolate приложение может принимать новые действия, пока прежнее вычисление продолжается. У этого улучшения есть продолжение, которого нет в таблице времени разбора.

Например, пользователь меняет условия поиска до завершения предыдущей обработки. Старый результат остаётся корректным для прежнего запроса, но уже не подходит текущему экрану. Если после каждого await безусловно обновлять представление, более свободный интерфейс всё равно может показывать устаревшие данные.

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

Гонки запросов и отмена в опыте не исследовались: стенд намеренно разрешает worker только одно активное задание. Из его результатов нельзя сделать вывод, что интерфейс теперь выдержит поток запросов на каждое нажатие клавиши.

Поэтому полезный результат ревью такого изменения — ясное описание поведения: какая работа перестаёт мешать экрану, что возвращается после её завершения и при каких условиях этот результат ещё нужен. Число миллисекунд проверяет часть этого описания. Остальная часть не возникает автоматически вместе с Isolate.run.

Для тяжёлого запроса из рассмотренного опыта новый isolate оказался достаточным, чтобы существенно сократить паузу анимации. Постоянный обработчик потребовал бы отдельного обоснования, связанного с повторным использованием или хранением данных. На маленьком входе из ста записей сопоставимой проблемы с кадрами не наблюдалось ни у одного варианта — там сам повод вводить новое место исполнения ещё нужно найти.

В задаче про застывающий индикатор нельзя требовать от исправления только более быстрого возврата списка. В задаче про долгую загрузку нельзя считать вращение индикатора достаточным результатом. А если экрану нужна небольшая подборка, стоит сначала обосновать, почему весь каталог вообще должен пересекать границу исполнения. Это три разных основания для изменения кода. Пока они смешаны в требование «ускорить JSON», даже правильно выбранный API может решать не ту задачу.


Эксперимент выполнен на MacBook Air M1, 16 ГБ, macOS 26.5.1, Flutter 3.44.7 / Dart 3.12.2, на батарее. Таблица в статье относится только к Flutter profile. Отдельная серия с заранее скомпилированным Dart-кодом (AOT), остальные размеры, прогрев и полный протокол находятся в материалах эксперимента. Исходные результаты, включая две исключённые попытки со скрытым окном, сохранены; выбросы по величине не удалялись. Работа обычного компьютера не изолирована лабораторно. Измерения не доказывают поведение на Android, iOS или Web, расход памяти и энергии либо задержку обработки ввода. На Web compute выполняет callback в текущем цикле событий, поэтому переносить туда native-выводы нельзя. Документация compute.


ArkTelos · Лаборатория
Telegram: Lab RU · Lab EN · ArkTelos RU · ArkTelos EN

Границы результата

  • Локальный опыт на MacBook Air M1, 16 ГБ, macOS 26.5.1, Flutter 3.44.7 / Dart 3.12.2, на батарее. Таблица относится к Flutter profile, по 50 запросов для каждой стратегии и размера. Нет измерений Android/iOS/Web, памяти, энергии, реакции на ввод, предпочтений пользователей, отмены и гонок запросов. Стоимость копирования отдельно не выделялась. Две попытки со скрытым окном исключены целиком и сохранены. Подготовка публикации не повторяла эксперимент.

CODE / DATA / AGENTS

Связанные эксперименты

experimentjson-isolates-trial

Сравнить время готовности полного списка и кадровые паузы при трёх способах исполнения обработки JSON.

Локальный опыт на MacBook Air M1, 16 ГБ, macOS 26.5.1, Flutter 3.44.7 / Dart 3.12.2, на батарее. Таблица относится к Flutter profile, по 50 запросов для каждой стратегии и размера. Нет измерений Android/iOS/Web, памяти, энергии, реакции на ввод, предпочтений пользователей, отмены и гонок запросов. Стоимость копирования отдельно не выделялась. Две попытки со скрытым окном исключены целиком и сохранены. Подготовка публикации не повторяла эксперимент.

Открыть паспорт эксперимента →

Версия в Telegram

Читать в ArkTelos Lab

Читать в ArkTelos Lab ↗

КАНАЛЫ ARKTELOS

Новости и инженерные материалы — без смешивания языков.

Основные каналы рассказывают о развитии ArkTelos. ArkTelos Lab публикует архитектурные разборы, эксперименты и воспроизводимые исследования.