HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>Во<strong>Flutter</strong>существует множество способов управления состоянием, но большинство из них строятся таким образом, что вся логика исполняется в главном изоляте вашего приложения. Исполнения сетевых запросов, работа с WebSocket, потенциально тяжелые синхронные операции (вроде локального поиска) все это, обычно, реализуют именно в главном изоляте. Эта статья покажет и другие двери.</p>
1 <p>Во<strong>Flutter</strong>существует множество способов управления состоянием, но большинство из них строятся таким образом, что вся логика исполняется в главном изоляте вашего приложения. Исполнения сетевых запросов, работа с WebSocket, потенциально тяжелые синхронные операции (вроде локального поиска) все это, обычно, реализуют именно в главном изоляте. Эта статья покажет и другие двери.</p>
2 <p>Мне попадался всего один пакет, предназначенный для вынесения этих операций во внешние изоляты, но недавно появился и<a>другой</a>написанный мной). Предлагаю вам с ним ознакомиться.</p>
2 <p>Мне попадался всего один пакет, предназначенный для вынесения этих операций во внешние изоляты, но недавно появился и<a>другой</a>написанный мной). Предлагаю вам с ним ознакомиться.</p>
3 <p>В данной статье я буду оперировать двумя основными терминами --<strong>изолят</strong>и<strong>главный поток</strong>. Они отличаются, чтобы текст не был слишком тавтологичен, но по существу, главный поток -- тоже изолят. Также тут вы найдете некоторые выражения, которые будут резать слух (или глаза) особенно чутким натурам, поэтому приношу заранее свои извинения -- извините. Также, называя в дальнейшем операции синхронными, я буду иметь в виду то, что результат вы будете получать в той же функции, в которой вызвали сторонний метод. А асинхронными -- такие функции, в которых на месте вы не получите результата, но получите его в другом.</p>
3 <p>В данной статье я буду оперировать двумя основными терминами --<strong>изолят</strong>и<strong>главный поток</strong>. Они отличаются, чтобы текст не был слишком тавтологичен, но по существу, главный поток -- тоже изолят. Также тут вы найдете некоторые выражения, которые будут резать слух (или глаза) особенно чутким натурам, поэтому приношу заранее свои извинения -- извините. Также, называя в дальнейшем операции синхронными, я буду иметь в виду то, что результат вы будете получать в той же функции, в которой вызвали сторонний метод. А асинхронными -- такие функции, в которых на месте вы не получите результата, но получите его в другом.</p>
4 <h2>Введение</h2>
4 <h2>Введение</h2>
5 <p>Изоляты предназначены для исполнения кода в не основном потоке вашего приложения. Когда основной поток начинает исполнять сетевые запросы, производить вычисления или делать какие угодно операции, отличные от его главного предназначения -- отрисовки интерфейса, рано или поздно вы столкнетесь с тем, что драгоценное время на отрисовку одного кадра начнет увеличиваться. В основном, время, доступное вам для выполнения любой операции в главном потоке, ограничено ~16ms -- это окно, существующее между отрисовкой 2-х кадров при частоте 60FPS. Однако, в данный момент есть множество телефонов с большей частотой дисплея, и так, как у меня как раз такой -- тем интереснее будет сравнить производительность приложения при одних и тех же действиях с использованием разных подходов. В таком случае, окно равно уже ~11.11ms, а частота обновления дисплея 90FPS.</p>
5 <p>Изоляты предназначены для исполнения кода в не основном потоке вашего приложения. Когда основной поток начинает исполнять сетевые запросы, производить вычисления или делать какие угодно операции, отличные от его главного предназначения -- отрисовки интерфейса, рано или поздно вы столкнетесь с тем, что драгоценное время на отрисовку одного кадра начнет увеличиваться. В основном, время, доступное вам для выполнения любой операции в главном потоке, ограничено ~16ms -- это окно, существующее между отрисовкой 2-х кадров при частоте 60FPS. Однако, в данный момент есть множество телефонов с большей частотой дисплея, и так, как у меня как раз такой -- тем интереснее будет сравнить производительность приложения при одних и тех же действиях с использованием разных подходов. В таком случае, окно равно уже ~11.11ms, а частота обновления дисплея 90FPS.</p>
6 <h2>Исходные данные</h2>
6 <h2>Исходные данные</h2>
7 <p>Представим, что вам необходимо загрузить большой объем данных, вы можете сделать это несколькими способами:</p>
7 <p>Представим, что вам необходимо загрузить большой объем данных, вы можете сделать это несколькими способами:</p>
8 <ul><li>Просто осуществить запрос в главном потоке</li>
8 <ul><li>Просто осуществить запрос в главном потоке</li>
9 <li>Использовать функцию compute для осуществления запроса</li>
9 <li>Использовать функцию compute для осуществления запроса</li>
10 <li>Явно использовать изолят для запроса</li>
10 <li>Явно использовать изолят для запроса</li>
11 </ul><p>Эксперименты проводились на смартфоне OnePlus 7 Pro, с процессором Snapdragon 855, и принудительно заданной частотой экрана в 90Hz. Приложение запускалось командой flutter run --profile. Проводилась эмуляция получения данных с сервера (5 одновременных запросов 10 раз подряд).</p>
11 </ul><p>Эксперименты проводились на смартфоне OnePlus 7 Pro, с процессором Snapdragon 855, и принудительно заданной частотой экрана в 90Hz. Приложение запускалось командой flutter run --profile. Проводилась эмуляция получения данных с сервера (5 одновременных запросов 10 раз подряд).</p>
12 <p>В одном запросе возвращается JSON -- массив из 2273 элементов, один из которых изображен на скриншоте. Размер ответа 1.12Mb. Таким образом, для 5 одновременных запросов получаем необходимость распарсить 5.6Mb JSON'а (но элементов в списке приложения будет 2273).</p>
12 <p>В одном запросе возвращается JSON -- массив из 2273 элементов, один из которых изображен на скриншоте. Размер ответа 1.12Mb. Таким образом, для 5 одновременных запросов получаем необходимость распарсить 5.6Mb JSON'а (но элементов в списке приложения будет 2273).</p>
13 <p>Параметры ответа сервера:</p>
13 <p>Параметры ответа сервера:</p>
14 <p>Давайте сравним все три способа по таким параметрам -- время отрисовки кадра, время операции, сложность организации/написания кода.</p>
14 <p>Давайте сравним все три способа по таким параметрам -- время отрисовки кадра, время операции, сложность организации/написания кода.</p>
15 <h2>Пример первый: Пачка запросов из главного потока</h2>
15 <h2>Пример первый: Пачка запросов из главного потока</h2>
16 <p>Есть следующий код:</p>
16 <p>Есть следующий код:</p>
17 Future&lt;void&gt; loadItemsOnMainThread() async { _startFpsMeter(); isLoading = true; notifyListeners(); List&lt;Item&gt; mainThreadItems; for (int i = 0; i &lt; 10; i++) { bench.startTimer('Load items in main thread'); mainThreadItems = await makeManyRequests(5); final double diff = bench.endTimer('Load items in main thread'); requestDurations.add(diff); } items.clear(); items.addAll(mainThreadItems); isLoading = false; notifyListeners(); _stopFpsMeter(); requestDurations.clear(); }<p>Данный метод находится в реактивном стейте, исполняемом в главном изоляте приложения.</p>
17 Future&lt;void&gt; loadItemsOnMainThread() async { _startFpsMeter(); isLoading = true; notifyListeners(); List&lt;Item&gt; mainThreadItems; for (int i = 0; i &lt; 10; i++) { bench.startTimer('Load items in main thread'); mainThreadItems = await makeManyRequests(5); final double diff = bench.endTimer('Load items in main thread'); requestDurations.add(diff); } items.clear(); items.addAll(mainThreadItems); isLoading = false; notifyListeners(); _stopFpsMeter(); requestDurations.clear(); }<p>Данный метод находится в реактивном стейте, исполняемом в главном изоляте приложения.</p>
18 <p>При выполнении кода выше получаем следующие значения:</p>
18 <p>При выполнении кода выше получаем следующие значения:</p>
19 <ul><li>Среднее время отрисовки одного кадра -- 14,036ms / 71.25FPS</li>
19 <ul><li>Среднее время отрисовки одного кадра -- 14,036ms / 71.25FPS</li>
20 <li>Медианное время кадра -- 11.148ms / 89.70FPS</li>
20 <li>Медианное время кадра -- 11.148ms / 89.70FPS</li>
21 <li>Максимальное время отрисовки одного кадра -- 100,332ms / 9.97FPS</li>
21 <li>Максимальное время отрисовки одного кадра -- 100,332ms / 9.97FPS</li>
22 <li>Среднее время для выполнения 5 одновременных запросов -- 226.894ms</li>
22 <li>Среднее время для выполнения 5 одновременных запросов -- 226.894ms</li>
23 </ul><h2>Пример второй: Compute</h2>
23 </ul><h2>Пример второй: Compute</h2>
24 Future&lt;void&gt; loadItemsWithComputed() async { _startFpsMeter(); isLoading = true; notifyListeners(); List&lt;Item&gt; computedItems; /// Реализовывались два варианта исполнения /// каждая пачка из 5 одновременных запросов, запускаемых последовательно, /// запускалась в функции compute if (true) { for (int i = 0; i &lt; 10; i++) { bench.startTimer('Load items in computed'); computedItems = await compute&lt;dynamic, List&lt;Item&gt;&gt;(_loadItemsWithComputed, null); final double diff = bench.endTimer('Load items in computed'); requestDurations.add(diff); } /// Второй вариант -- все 10 запросов по 5 штук в одной функции compute } else { bench.startTimer('Load items in computed'); computedItems = await compute&lt;dynamic, List&lt;Item&gt;&gt;(_loadAllItemsWithComputed, null); final double diff = bench.endTimer('Load items in computed'); requestDurations.add(diff); } items.clear(); items.addAll(computedItems); isLoading = false; notifyListeners(); _stopFpsMeter(); requestDurations.clear(); } Future&lt;List&lt;Item&gt;&gt; _loadItemsWithComputed([dynamic _]) async { return makeManyRequests(5); } Future&lt;List&lt;Item&gt;&gt; _loadAllItemsWithComputed([dynamic _]) async { List&lt;Item&gt; items; for (int i = 0; i &lt; 10; i++) { items = await makeManyRequests(5); } return items; }<p>В данном примере такие же запросы запускались в двух вариантах: каждые 5 одновременных запросов из 10 последовательных запускались каждый в своем compute:</p>
24 Future&lt;void&gt; loadItemsWithComputed() async { _startFpsMeter(); isLoading = true; notifyListeners(); List&lt;Item&gt; computedItems; /// Реализовывались два варианта исполнения /// каждая пачка из 5 одновременных запросов, запускаемых последовательно, /// запускалась в функции compute if (true) { for (int i = 0; i &lt; 10; i++) { bench.startTimer('Load items in computed'); computedItems = await compute&lt;dynamic, List&lt;Item&gt;&gt;(_loadItemsWithComputed, null); final double diff = bench.endTimer('Load items in computed'); requestDurations.add(diff); } /// Второй вариант -- все 10 запросов по 5 штук в одной функции compute } else { bench.startTimer('Load items in computed'); computedItems = await compute&lt;dynamic, List&lt;Item&gt;&gt;(_loadAllItemsWithComputed, null); final double diff = bench.endTimer('Load items in computed'); requestDurations.add(diff); } items.clear(); items.addAll(computedItems); isLoading = false; notifyListeners(); _stopFpsMeter(); requestDurations.clear(); } Future&lt;List&lt;Item&gt;&gt; _loadItemsWithComputed([dynamic _]) async { return makeManyRequests(5); } Future&lt;List&lt;Item&gt;&gt; _loadAllItemsWithComputed([dynamic _]) async { List&lt;Item&gt; items; for (int i = 0; i &lt; 10; i++) { items = await makeManyRequests(5); } return items; }<p>В данном примере такие же запросы запускались в двух вариантах: каждые 5 одновременных запросов из 10 последовательных запускались каждый в своем compute:</p>
25 <ul><li>Среднее время кадра -- 11.254ms / 88.86FPS</li>
25 <ul><li>Среднее время кадра -- 11.254ms / 88.86FPS</li>
26 <li>Медианное время кадра -- 11.152ms / 89.67FPS</li>
26 <li>Медианное время кадра -- 11.152ms / 89.67FPS</li>
27 <li>Максимальное время кадра -- 22.304ms / 44.84FPS</li>
27 <li>Максимальное время кадра -- 22.304ms / 44.84FPS</li>
28 <li>Среднее время для 5 одновременных запросов -- 386.253ms</li>
28 <li>Среднее время для 5 одновременных запросов -- 386.253ms</li>
29 </ul><p>Второй вариант -- все 10 последовательных запросов по 5 одновременных запускались в одном compute:</p>
29 </ul><p>Второй вариант -- все 10 последовательных запросов по 5 одновременных запускались в одном compute:</p>
30 <ul><li>Среднее время кадра -- 11.252ms / 88.87FPS</li>
30 <ul><li>Среднее время кадра -- 11.252ms / 88.87FPS</li>
31 <li>Медианное время кадра -- 11.152ms / 89.67FPS</li>
31 <li>Медианное время кадра -- 11.152ms / 89.67FPS</li>
32 <li>Максимальное время кадра -- 22.306ms / 44.83FPS</li>
32 <li>Максимальное время кадра -- 22.306ms / 44.83FPS</li>
33 </ul><p>Среднее время для 5 одновременных запросов (считалось, как выполнение всех 10 по 5 запросов в compute, деленное на 10) - 231.747ms</p>
33 </ul><p>Среднее время для 5 одновременных запросов (считалось, как выполнение всех 10 по 5 запросов в compute, деленное на 10) - 231.747ms</p>
34 <h2>Пример третий: Isolate</h2>
34 <h2>Пример третий: Isolate</h2>
35 <p>Тут стоит сделать отступление: в терминологии пакета существует две части общего стейта (состояния):</p>
35 <p>Тут стоит сделать отступление: в терминологии пакета существует две части общего стейта (состояния):</p>
36 <ol><li>Frontend-стейт -- некий реактивный стейт, который отправляет сообщения в Backend, обрабатывает его ответы, а также хранит данные, после обновления которых обновляется и UI, а также он хранит легкие методы, которые вызываются из UI. Данный стейт работает в главном потоке приложения.</li>
36 <ol><li>Frontend-стейт -- некий реактивный стейт, который отправляет сообщения в Backend, обрабатывает его ответы, а также хранит данные, после обновления которых обновляется и UI, а также он хранит легкие методы, которые вызываются из UI. Данный стейт работает в главном потоке приложения.</li>
37 <li>Backend-стейт -- тяжелый стейт, получающий сообщения от фронта, выполняющий тяжелые операции, возвращающий ответы фронту и работающий в отдельном изоляте. Данный стейт также может хранить данные (тут, как вам захочется).</li>
37 <li>Backend-стейт -- тяжелый стейт, получающий сообщения от фронта, выполняющий тяжелые операции, возвращающий ответы фронту и работающий в отдельном изоляте. Данный стейт также может хранить данные (тут, как вам захочется).</li>
38 </ol><p>Код из третьего варианта разбит на несколько методов, по причине наличия необходимости общения с изолятом. Методы фронта показаны ниже:</p>
38 </ol><p>Код из третьего варианта разбит на несколько методов, по причине наличия необходимости общения с изолятом. Методы фронта показаны ниже:</p>
39 /// Данный метод является точкой входа в операцию Future&lt;void&gt; loadItemsWithIsolate() async { /// Запускаем счетчик кадров перед всей операцией _startFpsMeter(); isLoading = true; notifyListeners(); /// Начинаем считать время запросов bench.startTimer('Load items in separate isolate'); /// Отправляем событие в "тяжеловесную" часть стейта, запускаемую на изоляте send(Events.startLoadingItems); } /// Обработчик события [Events.loadingItems] по обновлению времени запросов из изолята void _middleLoadingEvent() { final double time = bench.endTimer('Load items in separate isolate'); requestDurations.add(time); bench.startTimer('Load items in separate isolate'); } /// Обработчик завершающего события [Events.endLoadingItems] из изолята Future&lt;void&gt; _endLoadingEvents(List&lt;Item&gt; items) async { this.items.clear(); /// Обновляем данные в реактивном стейте this.items.addAll(items); /// Заканчиваем считать время запросов final double time = bench.endTimer('Load items in separate isolate'); requestDurations.add(time); isLoading = false; notifyListeners(); /// Останавливаем счетчик кадров _stopFpsMeter(); requestDurations.clear(); }<p>А тут вы можете увидеть метод бэка с нужной нам логикой:</p>
39 /// Данный метод является точкой входа в операцию Future&lt;void&gt; loadItemsWithIsolate() async { /// Запускаем счетчик кадров перед всей операцией _startFpsMeter(); isLoading = true; notifyListeners(); /// Начинаем считать время запросов bench.startTimer('Load items in separate isolate'); /// Отправляем событие в "тяжеловесную" часть стейта, запускаемую на изоляте send(Events.startLoadingItems); } /// Обработчик события [Events.loadingItems] по обновлению времени запросов из изолята void _middleLoadingEvent() { final double time = bench.endTimer('Load items in separate isolate'); requestDurations.add(time); bench.startTimer('Load items in separate isolate'); } /// Обработчик завершающего события [Events.endLoadingItems] из изолята Future&lt;void&gt; _endLoadingEvents(List&lt;Item&gt; items) async { this.items.clear(); /// Обновляем данные в реактивном стейте this.items.addAll(items); /// Заканчиваем считать время запросов final double time = bench.endTimer('Load items in separate isolate'); requestDurations.add(time); isLoading = false; notifyListeners(); /// Останавливаем счетчик кадров _stopFpsMeter(); requestDurations.clear(); }<p>А тут вы можете увидеть метод бэка с нужной нам логикой:</p>
40 /// Обработчик события [Events.startLoadingItems] Future&lt;void&gt; _loadingItems() async { _items.clear(); for (int i = 0; i &lt; 10; i++) { _items.addAll(await makeManyRequests(5)); if (i &lt; (10 - 1)) { /// Для всех запросов, кроме последнего - отсылаем только одно событие send(Events.loadingItems); } else { /// Для последнего из 10ти запросов - отсылаем сообщение с данными send(Events.endLoadingItems, _items); } } }<p>Результаты:</p>
40 /// Обработчик события [Events.startLoadingItems] Future&lt;void&gt; _loadingItems() async { _items.clear(); for (int i = 0; i &lt; 10; i++) { _items.addAll(await makeManyRequests(5)); if (i &lt; (10 - 1)) { /// Для всех запросов, кроме последнего - отсылаем только одно событие send(Events.loadingItems); } else { /// Для последнего из 10ти запросов - отсылаем сообщение с данными send(Events.endLoadingItems, _items); } } }<p>Результаты:</p>
41 <ul><li>Среднее время кадра -- 11.151ms / 89.68FPS</li>
41 <ul><li>Среднее время кадра -- 11.151ms / 89.68FPS</li>
42 <li>Медианное время кадра -- 11.151ms / 89.68FPS</li>
42 <li>Медианное время кадра -- 11.151ms / 89.68FPS</li>
43 <li>Максимальное время кадра -- 11.152ms / 89.67FPS</li>
43 <li>Максимальное время кадра -- 11.152ms / 89.67FPS</li>
44 </ul><h2>Промежуточные итоги</h2>
44 </ul><h2>Промежуточные итоги</h2>
45 <p>Проведя три эксперимента по загрузке в приложении одного и того же набора данных получаем такие показатели:</p>
45 <p>Проведя три эксперимента по загрузке в приложении одного и того же набора данных получаем такие показатели:</p>
46 <p>Судя по данным цифрам, можно сделать следующие выводы:</p>
46 <p>Судя по данным цифрам, можно сделать следующие выводы:</p>
47 <ul><li>Flutter способен обеспечивать стабильные ~90FPS</li>
47 <ul><li>Flutter способен обеспечивать стабильные ~90FPS</li>
48 <li>Осуществление множества тяжелых сетевых запросов в главном потоке вашего приложения сказывается на его производительности -- появляются лаги</li>
48 <li>Осуществление множества тяжелых сетевых запросов в главном потоке вашего приложения сказывается на его производительности -- появляются лаги</li>
49 <li>Написание кода, исполняемого в главном потоке проще простого</li>
49 <li>Написание кода, исполняемого в главном потоке проще простого</li>
50 <li>Compute позволяет уменьшить заметность лагов</li>
50 <li>Compute позволяет уменьшить заметность лагов</li>
51 <li>Написание кода с использованием Compute несет некоторые ограничения (чистые функции, нельзя передавать статические методы, нет замыкания и т.д.)</li>
51 <li>Написание кода с использованием Compute несет некоторые ограничения (чистые функции, нельзя передавать статические методы, нет замыкания и т.д.)</li>
52 <li>Overhead при использовании compute по времени операции ~150-160ms</li>
52 <li>Overhead при использовании compute по времени операции ~150-160ms</li>
53 <li>Isolate позволяет полностью избавиться от лагов</li>
53 <li>Isolate позволяет полностью избавиться от лагов</li>
54 <li>Написание кода с использованием изолятов сложнее, и также несет некоторые ограничения, о которых позднее</li>
54 <li>Написание кода с использованием изолятов сложнее, и также несет некоторые ограничения, о которых позднее</li>
55 </ul><p>Давайте проведем еще один эксперимент, чтобы узнать наверняка, какой из способов оптимален по всем исследуемым параметрам. Его результат -- в<a>следующей части</a>нашей статьи.</p>
55 </ul><p>Давайте проведем еще один эксперимент, чтобы узнать наверняка, какой из способов оптимален по всем исследуемым параметрам. Его результат -- в<a>следующей части</a>нашей статьи.</p>
56  
56