
— Парни, у нас PWA тормозит! — в голосе Димы чувствовались нотки интриги.
Вообще-то мы разрабатываем на Flutter кроссплатформенное приложение для мобильных устройств, но коль уж фреймворк позволяет, на сдачу запустили и веб-версию. Поначалу с PWA мы отхватили немало проблем, но со временем большую часть из них победили. Только вот производительность (из песни слов не выкинешь) так и осталась ахиллесовой пятой приложения — даже на достаточно мощных устройствах нет-нет, да проскакивали микрофризы.
Новостью это не было ни для нас, ни для Димы, поэтому причина озвучивания этого факта ни с того, ни с сего была не ясна.
Вдоволь насладившись нашими удивлёнными взорами, Дима продолжил:
— Нет, вы не поняли. У нас приложение очень тормозит — слово "очень" он произнёс с акцентом на "о", ещё и протянув его пару секунд, чтобы подчеркнуть, насколько всё плохо. — Пользователи жалуются, что после авторизации может зависнуть на несколько секунд, иногда до десяти.
Вот это уже было неожиданностью. Разработчики сдержанно загудели, обсуж��ая возможные причины.
Дима подождал, когда схлынут самые жаркие споры и уточнил:
— Что думаете делать?
— А что тут думать, профилировать надо — высказал витавшую в воздухе мысль Саша.
Профилируем
У Flutter есть довольно неплохие инструменты разработчика, которые включают в том числе и профилировщик. Однако Саша знал, что в мобильных приложениях проблема с лагом была выражена не так сильно, как в PWA, поэтому и профилировать решил с помощью инструментов разработчика Chrome.
Дело это нехитрое: запускаем веб версию приложения в режиме профилирования, открываем инструменты разработчика в браузере, переходим на вкладку "Perfomance" и в нужном месте включаем запись. После прохождения интересующего сценария в приложении останется остановить запись и подождать, когда Chrome подготовит данные.
В нашем случае источник проблемы был найден довольно быстро: им оказалась функция из сторонней библиотеки, которая занимается длительными математическими вычислениями. Классическая числодробилка, вроде нахождения числа Фибоначчи или факториала.
Выглядела эта функция примерно так:
void _onPressed() {
setState(() => _inProgress = true);
// Вызов функции, которая долго вычисляется
_result = fib(_value);
setState(() => _inProgress = false);
}Вызов этой функции в вебе измерялся секундами, а бедный пользователь всё это время с тоской и тревогой наблюдал застывший экран.

Хорошо, что во Flutter есть волшебная функция compute! С её помощью можно вынести долгие вычисления в отдельный изолят, а когда вычисления будут завершены — положить их в нужное место. Понятно дело, что магия в этом мире работает плохо и получить искомое значение быстрее не получится, но хотя бы можно не блокировать основной поток. В этом случае пользователь вместо унылого застывшего экрана будет видеть экран с весёлым и задорным лоадером.
Саша уже умел пользоваться функцией compute, поэтому приступил к переписыванию кода.
Используем compute
Функция compute принимает два аргумента: вычисляемую функцию и её аргумент, а возвращает объект Future. С помощью метода then этого объекта мы можем присвоить нужное значение нашим переменным, когда вычисления будут закончены.
После переработки наш метод принял следующий вид:
void _onPressed() {
setState(() => _inProgress = true);
// Вызов функции, которая долго вычисляется
compute((n) => fib(n), _value).then((value) {
_result = value;
setState(() => _inProgress = false);
});
}Получилось неплохо! — подумал довольный собой Саша, после того как запустил приложение на десктопе (обычно при разработке он запускал приложение на своём горячо любимом линуксе — так было быстрее). А чего бы ему не быть довольным? — кнопки нажимаются, лоадеры задорно крутятся и в нужный момент меняются на задуманные виджеты. Никаких тебе фризов.
Мысль о фризах напомнила Саше о лежащем в холодильнике мороженом. "За хорошую работу не грех себя порадовать вкусняшкой — особенно в такую жару", подумал Саша и сходил за холодной сладостью.
Однако посл�� запуска веб-версии приложения мороженое пришлось отставить в сторону: там лаги и фризы будто бы не знали, что им следовало исчезнуть. Профилировщик говорил то же, что и раньше: вы долго считаете какую-то дрянь.

Если что-то ведёт себя не так, как ты ожидаешь, то скорее всего ты чего-то не знаешь об этом самом чём-то. И действительно, если обратиться к статье об изолятах на сайте языка Dart, то можно обнаружить такое сообщение:
All Dart apps can use
async-await,Future, andStreamfor non-blocking, interleaved computations. The Dart web platform, however, does not support isolates.
Перефразируя классиков, можно сказать, что товарищ compute в вебе нам совсем не товарищ.
Как оказалось, мы были не единственными, кто столкнулся с проблемой производительности при работе с данной библиотекой: в репозитории обнаружился issue двухгодичной давности, в котором обсуждалась эта проблема. Авторы библиотеки честно сказали, что для веба ничего сделать нельзя, разве что переписать её на WASM.
Мысль о реализации нужной функциональности для веба на технологии WebAssembly показалась интересной, ведь это стильно-модно-молодёжно, да и команда Flutter/Dart недавно рассказывала, что работает над этой технологией.
Прикручиваем WASM
Беглый поиск, однако, показал, что для Dart эта технология пока не готова к продакшену: поддерживают её только последние версии Chrome и Firefox, а Safari не поддерживает вовсе (и похоже не собирается).
С другой стороны, некоторые языки программирования уже умеют работать с данной технологией (например, Rust). Саша не был знатоком Rust, но этого и не требовалось: используемая функция наверняка уже был реализована для этого языка, нужно было только скомпилировать её в wasm и научить PWA использовать этот wasm-файл.
Шаг первый: собираем wasm модуль
Одна из приятных особенностей работы с экосистемой Rust — это хорошая документация. О том как подготовить Rust проект для компиляции в wasm очень подробно описано здесь. Важный момент, который там описан, это установка wasm-pack.
Создадим новый Rust проект:
cargo new --lib fib_wasmТеперь нужно добавить в файл Cargo.toml новую зависимость:
[dependencies]
wasm-bindgen = "0.2"Именно библиотека wasm-bindgen возьмёт на себя всю работу по подготовке Rust кода в wasm: для этого используется атрибут #[wasm_bindgen]:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fib(n: u32) -> u32 {
if n < 2 {
n
} else {
fib(n - 2) + fib(n - 1)
}
}Чтобы собрать проект в wasm, нужно выполнить команду:
wasm-pack build --target webПосле этого в каталоге с проектом появится папка pkg, в которой нас интересуют файлы <project_name>.js и <project_name>_bg.wasm.
Осталось придумать, как научить PWA использовать получившийся wasm файл.
Шаг второй: прикручиваем wasm к html
Для начала давайте положим полученные на предыдущем шаге файлы куда-нибудь поближе к index.html (можно просто в каталог web) и добавим в этот файл следующие строки:
<script type="module" defer="">
import init, { fib } from "./fib_wasm.js";
init().then(() => {
window.fib = (n) => fib(n)
});
</script>Здесь мы добавили к объекту window нашу функцию, чтобы иметь возможность достучаться до неё из js кода.
Теперь необходимо подготовить две реализации вызова искомой функции: для веб-платформы и остальных, а затем, с помощью условного импорта, обратиться к нужной реализации.
В нашем случае это будут файлы fib.dart:
/// Сторонняя функция, которая долго вычисляется
int fib(int n) => switch (n) {
< 2 => n,
_ => fib(n - 2) + fib(n - 1),
};И fib_web.dart:
// ignore: avoid_web_libraries_in_flutter
import 'dart:js';
/// Сторонняя функция, которая долго вычисляется
int fib(int n) {
return context.callMethod('fib', [n]);
}Здесь мы обращаемся к той самой функции, которую добавили к объекту window.
Осталось внести изменения в наш исходный проект:
import 'fib.dart' if (dart.library.html) 'fib_web.dart';Теперь можно и посмотреть, насколько быстрее стал наш код:

Ну что же, неплохо! В данном случае вместо 6 секунд результат был получен за 0,945 секунды. В зависимости от особенностей вычисляемой функции, результаты могут отличаться в большую или меньшую сторону: например, в одной нашей задаче время изменилось с 1,5-8 секунд до 0,05-0,09 секунды (sic!).
В веб этот код всё ещё выполняется в основном потоке, т.е. по-прежнему может вызывать фризы, но хотя бы на меньшее время.
Заключение
Flutter — хорошая технология, которая действительно позволяет создавать крутые кроссплатформенные приложения. Но как и у любой технологии, у неё есть ограничения (особенно на не мобильных платформах). Однако технологии не стоят на месте и иногда, если ваше приложение споткнулось, ему могут помочь коллеги по цеху. Такие как Rust и WebAssembly.
Исходники проекта можно посмотреть на GitHub.
P.S. Все события вымышлены, любые совпадения с реальными людьми случайны. За время написания статьи ни одно мороженое не пострадало.
