Когда мы апгрейдим процессор или видеокарту и начинаем играть в привычную нам игрушку с новым железом, то увеличение производительности на некоторое количество процентов становится сразу же заметным. Например, увеличение производительности заметно уже с 5 процентов, если дело касается FPS в какой-то игре. 10 процентов — более заметно, 15 процентов — ещё заметнее. А увеличение производительности на 50 процентов не заметить практически невозможно. Примерно так же я смотрел на отрисовку интерфейса при компиляции Flutter-приложений в WebAssembly-код вместо JavaScript. Примерно по таким же ощущениям для большого проекта я получил ускорение, ну, примерно от 5 до 10 процентов. Хотя должен был получить 50. Поэтому я решил очень подробно исследовать данную ситуацию.Я решил досконально проверить, насколько можно улучшить веб приложения переходом на компиляцию в WASM вместо JS, в отличии от официальных цифр у меня получились другие цифры и результат работы приложения их подтверждает.После перехода на Wasm приложение может стать лишь немного быстрее. Я собрал отдельный Flutter-стенд, чтобы разобраться, откуда берётся этот результат: какие операции ускоряются, где выигрывает JavaScript и как вычисления связаны с плавностью интерфейса. Читать далее
Когда мы апгрейдим процессор или видеокарту и начинаем играть в привычную нам игрушку с новым железом, то увеличение производительности на некоторое количество процентов становится сразу же заметным. Например, увеличение производительности заметно уже с 5 процентов, если дело касается FPS в какой-то игре. 10 процентов — более заметно, 15 процентов — ещё заметнее. А увеличение производительности на 50 процентов не заметить практически невозможно. Примерно так же я смотрел на отрисовку интерфейса при компиляции Flutter-приложений в WebAssembly-код вместо JavaScript. Примерно по таким же ощущениям для большого проекта я получил ускорение, ну, примерно от 5 до 10 процентов. Хотя должен был получить 50. Поэтому я решил очень подробно исследовать данную ситуацию.
Я решил досконально проверить, насколько можно улучшить веб приложения переходом на компиляцию в WASM вместо JS, в отличии от официальных цифр у меня получились другие цифры и результат работы приложения их подтверждает.
После перехода на Wasm приложение может стать лишь немного быстрее. Я собрал отдельный Flutter-стенд, чтобы разобраться, откуда берётся этот результат: какие операции ускоряются, где выигрывает JavaScript и как вычисления связаны с плавностью интерфейса.
На чём и как проводился экспериментКомпьютер: Apple M4, 16 ГиБ оперативной памяти, arm64, Darwin 24.3.0.
Браузер: Chrome/154.0.8037.98, движок V8 15.4.80.20.
Разработка: Flutter 3.47.1, Dart 3.13.1, сборки release.
Графика: ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Version 15.3.1 (Build 24D70)). Окно 1280 × 900 CSS-пикселей, DPR 1.
Размещение: локальный сервер на том же компьютере. Все основные и дополнительные замеры выполнены в настоящем настольном Chrome.
Из одного Dart-кода получены две веб-сборки. В JavaScript-варианте Flutter рисует через CanvasKit. Wasm-вариант использует Skwasm; его я запускал с отрисовкой в основном потоке и в отдельном рабочем потоке. В таблицах они обозначены JavaScript, Wasm ST и Wasm MT.
Основная серия содержит шесть сессий каждого режима и семь замеров задачи в каждой сессии. Порядок режимов менялся, чтобы запуск первым или последним распределялся между ними. Для семи пограничных и нестабильных случаев я выполнил расширенную серию: 12 сессий на режим, 15 предварительных пакетов и затем 21 учитываемый замер на задачу.
Задачи охватывают числовые вычисления, память, объекты, текст, браузерные службы и Flutter-интерфейс. Под каждым кейсом есть код из измеренного стенда. Общие классы подготовки данных и полный измерительный цикл собраны в конце статьи. Все численные результаты рассчитаны по сохранённым замерам.
Как читать результатыВремя указано в миллисекундах на один повтор описанной задачи. Меньшее значение означает более быстрое выполнение. Типичное время получено медианой: сначала внутри сессии, затем по сессиям. Очень короткие задачи выполнялись многократно в одном пакете; его длительность делилась на количество повторов.
Коэффициент JS / Wasm показывает отношение времени. Например, значение 2 означает, что Wasm выполнил работу вдвое быстрее. Коэффициент 0,5 означает двукратный выигрыш JavaScript. Коэффициенты вычисляются по парным сессиям, поэтому могут слегка отличаться от деления двух соседних значений в таблице.
СодержаниеПлавность и время кадра · Запуск приложения · Практические выводы · Подробности измерений · Сводная таблица · Полные исходники
Вычисления: числа, матрицы, изображения и звук1. Вычисления с целыми числамиВ обработке двоичных данных часто приходится сдвигать биты, применять маски и смешивать значения. Для такого типа работы выбран xorshift — небольшой алгоритм, в котором почти всё время занимают целочисленные операции.
Выполняются 300 000 шагов. После каждого шага результат ограничивается 32 битами. Это сохраняет одинаковый смысл вычисления в обеих сборках. Выигрыш относится к длинной последовательности таких операций; полное время обработки файла дополнительно включает чтение и запись. В одном измерительном пакете задача повторялась 54 раза.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 1,678 | 0,617 | 0,615 |
Основная серия. Wasm быстрее в 2,72 раза. JS / Wasm ST: 2,72× [2,71; 2,72].
Dart-код этого тестаint bitMix(int seed, int count) {
var x = seed;
for (var i = 0; i < count; i++) {
x = (x ^ (x << 13)) & 0xffffffff;
x = (x ^ (x >> 17)) & 0xffffffff;
x = (x ^ (x << 5)) & 0xffffffff;
}
return x;
}
Kernel(
'int32',
'Вычисления',
'Целые: xorshift / битовые операции',
'300 000 шагов; u32',
() => bitMix(data.seed, 300000),
)
Представим, что нужно вычислить сходство двух числовых векторов или вклад нескольких сигналов в общий результат. Для этого элементы двух массивов попарно перемножают и складывают.
В тесте перемножаются 65 536 пар чисел из Float64List, после чего произведения складываются простым циклом. Массивы подготовлены заранее. В одном измерительном пакете задача повторялась 143 раза.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,078 | 0,241 | 0,239 |
Основная серия. JavaScript быстрее в 3,09 раза. JS / Wasm ST: 0,32× [0,32; 0,34].
Dart-код этого тестаdouble dot(List<double> a, List<double> b) {
var sum = 0.0;
for (var i = 0; i < a.length; i++) {
sum += a[i] * b[i];
}
return sum;
}
Kernel(
'double_dot',
'Вычисления',
'Float64: скалярное произведение',
'65 536 пар',
() => dot(a, b),
)
Синусы и косинусы встречаются в анимации, геометрии и обработке сигналов. Квадратный корень нужен, например, при вычислении расстояния.
Для каждого из 30 000 аргументов выполняется выражение sin(x) × cos(x) + sqrt(x). В одном измерительном пакете задача повторялась 77 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,252 | 0,255 | 0,254 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 1,00× [0,99; 1,01].
Dart-код этого тестаKernel(
'transcendental',
'Вычисления',
'sin / cos / sqrt',
'30 000 аргументов',
() {
var sum = 0.0;
for (var i = 0; i < 30000; i++) {
final x = a[i];
sum += math.sin(x) * math.cos(x) + math.sqrt(x);
}
return sum;
},
)
Умножение матриц встречается в преобразованиях координат и численных алгоритмах. В этом опыте используются две квадратные матрицы размером 64 × 64.
Функция выделяет память под результат, выполняет три вложенных цикла и затем суммирует элементы полученной матрицы. Порядок циклов i–k–j обеспечивает последовательный проход по строкам. В одном измерительном пакете задача повторялась 63 раза.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,307 | 0,443 | 0,443 |
Основная серия. JavaScript быстрее в 1,44 раза. JS / Wasm ST: 0,69× [0,67; 0,72].
Dart-код этого тестаFloat64List multiply(List<double> a, List<double> b, int n) {
final out = Float64List(n * n);
for (var i = 0; i < n; i++) {
for (var k = 0; k < n; k++) {
final v = a[i * n + k];
for (var j = 0; j < n; j++) {
out[i * n + j] += v * b[k * n + j];
}
}
}
return out;
}
Kernel(
'matrix',
'Вычисления',
'Матрицы: обычный тройной цикл',
'64×64; Float64; включая output',
() => multiply(matrixA, matrixB, 64).fold(0.0, (x, y) => x + y),
)
Простой способ сгладить изображение — заменить яркость пикселя средним значением его соседей. Здесь для каждой внутренней точки берётся квадрат 3 × 3.
Тест обрабатывает числовое поле 256 × 256 на процессоре. Граничные пиксели пропускаются; вычисленные средние складываются в контрольную сумму. Замер охватывает арифметику и обращения к соседним элементам уже подготовленного массива. В редакторе изображений к этому добавятся декодирование файла и показ результата. В одном измерительном пакете задача повторялась 34 раза.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,349 | 0,184 | 0,182 |
Основная серия. Wasm быстрее в 1,88 раза. JS / Wasm ST: 1,88× [1,86; 1,97].
Dart-код этого тестаint stencil(Uint32List image, int width) {
var sum = 0;
for (var y = 1; y < width - 1; y++) {
for (var x = 1; x < width - 1; x++) {
final p = y * width + x;
sum +=
(image[p - width - 1] +
image[p - width] +
image[p - width + 1] +
image[p - 1] +
image[p] +
image[p + 1] +
image[p + width - 1] +
image[p + width] +
image[p + width + 1]) ~/
9;
}
}
return sum;
}
Kernel(
'image_stencil',
'Вычисления',
'Изображение: box-filter 3×3',
'256×256; без декодера/GPU',
() => stencil(data.values, 256),
)
При фильтрации звука текущий отсчёт можно вычислять как взвешенную сумму предыдущих. Это основа FIR-фильтра.
Подготовлены массивы из 8 192 отсчётов и 32 коэффициентов. Цикл начинает работу с позиции 32, перемножает отсчёты с коэффициентами и накапливает контрольную сумму. Работа аудиоустройства и расписание воспроизведения относятся к следующему уровню приложения. В одном измерительном пакете задача повторялась 50 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,163 | 0,169 | 0,168 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 0,96× [0,95; 1,03].
Dart-код этого тестаKernel(
'audio_fir',
'Вычисления',
'Аудио: FIR-фильтр',
'8 192 отсчёта × 32 taps',
() {
var sum = 0.0;
for (var i = 32; i < 8192; i++) {
for (var j = 0; j < 32; j++) {
sum += a[i - j] * b[j];
}
}
return sum;
},
)
Обработка записей часто содержит несколько правил: для малых значений одно действие, для средних другое, для больших третье. Такой код нагружает условные переходы.
262 144 значения проходят через три ветви if/else. Распределение входов фиксировано, поэтому сборки получают одну и ту же последовательность решений. Скорость такого кода чувствительна к расположению ветвей и профилю оптимизации. В одном измерительном пакете задача повторялась 19 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,631 | 0,809 | 0,654 |
Расширенная серия | 0,564 | 0,712 | 0,795 |
Основная серия. Скорость заметно меняется между запусками. JS / Wasm ST: 0,77× [0,69; 0,97].
Расширенная серия. Скорость заметно меняется между запусками. JS / Wasm ST: 0,81× [0,71; 1,00].
Dart-код этого тестаKernel(
'branch',
'Вычисления',
'Условные переходы по данным',
'262 144 значений',
() {
var sum = 0;
for (var i = 0; i < 262144; i++) {
final v = data.values[i];
if (v < 20000) {
sum += v * 3;
} else if (v < 40000) {
sum -= v;
} else {
sum += v ~/ 3;
}
}
return sum;
},
)
Рекурсивная функция вызывает сама себя. Подобная структура встречается при обходе дерева и разборе вложенных выражений. Для создания большого числа вызовов выбран рекурсивный Fibonacci(25).
Этот вариант многократно повторяет одни и те же вычисления, поэтому хорошо проявляет стоимость вызова и возврата. Для прикладного вычисления чисел Фибоначчи разумнее использовать линейный алгоритм. Здесь алгоритм служит нагрузкой на механизм вызовов. В одном измерительном пакете задача повторялась 48 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,505 | 0,130 | 0,128 |
Основная серия. Wasm быстрее в 3,89 раза. JS / Wasm ST: 3,89× [3,84; 3,90].
Dart-код этого тестаint fibonacci(int n) => n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
Kernel(
'recursion',
'Вычисления',
'Рекурсивные вызовы: Fibonacci',
'n=25',
() => fibonacci(25),
)
Обычных чисел бывает мало, когда нужна точная арифметика с длинными целыми. Dart предоставляет для этого BigInt.
Выполняются 1 000 шагов: квадрат текущего значения, прибавление номера шага и остаток по модулю 2¹²⁷ − 1. В обеих сборках используется одна и та же Dart-библиотека и сохраняется точность. В одном измерительном пакете задача повторялась 15 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 1,319 | 0,475 | 0,475 |
Основная серия. Wasm быстрее в 2,78 раза. JS / Wasm ST: 2,78× [2,75; 2,84].
Dart-код этого тестаKernel(
'bigint',
'Вычисления',
'BigInt: точная арифметика',
'1 000 modular multiply; 127 бит',
() {
var x = BigInt.from(data.seed);
for (var i = 0; i < 1000; i++) {
x = (x * x + BigInt.from(i)) % big;
}
return (x & BigInt.from(0x7fffffff)).toInt();
},
)
Этот тест измеряет сложение массива целых чисел обычным циклом.
Массив содержит 262 144 значения, ограниченных маской 4095. Простой цикл накапливает сумму в int. Вариант Int32x4 обрабатывает те же числа группами по четыре. В одном измерительном пакете задача повторялась 105 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,100 | 0,108 | 0,108 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 0,92× [0,91; 0,95].
Dart-код этого тестаKernel('dart_scalar_vector','Вычисления','Dart int: контроль суммы векторов','262 144 целых; тот же результат',(){
var sum=0;for(var i=0;i<scalarLanes.length;i++) {sum+=scalarLanes[i];}return sum;
})
Int32x4 позволяет выразить операцию над четырьмя 32-битными числами одновременно. В этой задаче складываются 65 536 таких групп.
После прохода четыре компонента результата объединяются в одну сумму. В Wasm-сборке этот код быстрее, чем в JavaScript-сборке. При сравнении с обычным циклом внутри Wasm выигрывает обычный цикл: стоимость доступа к векторным значениям и их представлениям влияет на время всей задачи. В одном измерительном пакете задача повторялась 48 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,349 | 0,190 | 0,187 |
Основная серия. Wasm быстрее в 1,85 раза. JS / Wasm ST: 1,85× [1,74; 1,89].
Dart-код этого тестаKernel('dart_vector','Вычисления','Dart Int32x4: сумма векторов','65 536 векторов; API не гарантирует SIMD',(){
var sum=Int32x4(0,0,0,0);for(var i=0;i<lanes.length;i++) {sum=sum+lanes[i];}return sum.x+sum.y+sum.z+sum.w;
})
Небольшие массивы встречаются в локальных таблицах и временных буферах. Здесь проверяется последовательное чтение 16 КиБ данных.
Все 4 096 значений складываются в сумму. Одна операция занимает несколько микросекунд, поэтому она повторяется в пакете. При таком масштабе становятся особенно заметны прогрев, оптимизация цикла и накладные расходы измерения. В одном измерительном пакете задача повторялась 512 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,003 | 0,002 | 0,002 |
Расширенная серия | 0,003 | 0,003 | 0,003 |
Основная серия. Скорость заметно меняется между запусками. JS / Wasm ST: 1,23× [1,00; 1,25].
Расширенная серия. Скорость практически одинакова. JS / Wasm ST: 0,97× [0,96; 1,00].
Dart-код этого тестаfor (final length in [4096, 1048576])
Kernel(
'memory_$length',
'Память',
'Последовательное чтение ${length == 4096 ? '16 KiB' : '4 MiB'}',
'$length × Uint32',
() {
var sum = 0;
for (var i = 0; i < length; i++) {
sum += data.values[i];
}
return sum;
},
)
Тот же последовательный проход выполнен по более крупному массиву: 4 МиБ, или 1 048 576 целых чисел.
Время включает чтение и суммирование. Оно описывает конкретный проход по массиву; для измерения предельной пропускной способности памяти используются существенно иные объёмы и схемы доступа. В одном измерительном пакете задача повторялась 35 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,642 | 0,531 | 0,529 |
Основная серия. Wasm быстрее в 1,20 раза. JS / Wasm ST: 1,20× [1,20; 1,22].
Dart-код этого тестаfor (final length in [4096, 1048576])
Kernel(
'memory_$length',
'Память',
'Последовательное чтение ${length == 4096 ? '16 KiB' : '4 MiB'}',
'$length × Uint32',
() {
var sum = 0;
for (var i = 0; i < length; i++) {
sum += data.values[i];
}
return sum;
},
)
При проходе по связанным данным адрес следующего элемента становится известен только после чтения текущего. Такой порядок затрудняет заблаговременную загрузку данных процессором.
В массиве размером 1 МиБ выполнены 262 144 зависимых перехода. Таблица ссылок формируется воспроизводимо и охватывает весь набор. В одном измерительном пакете задача повторялась 17 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 1,388 | 1,471 | 1,487 |
Расширенная серия | 1,249 | 1,562 | 1,534 |
Основная серия. Скорость заметно меняется между запусками. JS / Wasm ST: 0,90× [0,78; 0,97].
Расширенная серия. JavaScript быстрее в 1,17 раза. JS / Wasm ST: 0,85× [0,72; 0,91].
Dart-код этого тестаKernel(
'random_memory',
'Память',
'Зависимое случайное чтение',
'1 MiB; 262 144 переходов',
() {
var p = data.seed & (links.length - 1), sum = 0;
for (var i = 0; i < links.length; i++) {
p = links[p];
sum += p;
}
return sum;
},
)
Перед отправкой или обработкой данных приложение часто копирует буфер. Проверяем копирование 4 МиБ из одного типизированного массива в другой.
Приёмный массив выделен заранее. Внутри замера вызывается setAll, затем читаются крайние элементы для контрольного результата. В одном измерительном пакете задача повторялась 278 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,059 | 0,070 | 0,070 |
Основная серия. JavaScript быстрее в 1,20 раза. JS / Wasm ST: 0,83× [0,83; 0,84].
Dart-код этого тестаKernel(
'bulk_copy',
'Память',
'TypedData.setAll: копирование',
'4 MiB; заранее выделенный output',
() {
copy.setAll(0, data.values);
return copy[0] + copy[copy.length - 1];
},
)
При преобразовании данных создаётся много временных объектов: обёртки строк, промежуточные результаты и модели для отображения. Они быстро становятся доступными для удаления.
Тест создаёт 32 партии по 1 024 объекта Cell и читает их значения. Время включает выделение объектов и обход каждой партии. Работа сборщика мусора может попадать в отдельные замеры и отражается в их распределении. В одном измерительном пакете задача повторялась 91 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,101 | 0,169 | 0,169 |
Основная серия. JavaScript быстрее в 1,68 раза. JS / Wasm ST: 0,59× [0,59; 0,60].
Dart-код этого тестаKernel(
'allocation_short',
'Объекты',
'Короткоживущие объекты',
'32 партии × 1 024 Cell',
() {
var sum = 0;
for (var round = 0; round < 32; round++) {
final cells = List.generate(
1024,
(i) => Cell(data.values[i + round]),
);
for (final c in cells) {
sum += c.value;
}
}
return sum;
},
)
Связанные списки и деревья хранят ссылки на другие объекты. Чтобы проверить этот механизм, создаётся цепочка из 32 768 объектов.
Сначала строится вся цепочка, затем выполняется проход по ссылкам и суммирование значений. В отличие от последовательного числового массива здесь участвуют выделение объектов, их размещение и чтение ссылок. В одном измерительном пакете задача повторялась 91 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,078 | 0,084 | 0,084 |
Расширенная серия | 0,078 | 0,087 | 0,086 |
Основная серия. Разница находится около принятого порога 10%. JS / Wasm ST: 0,92× [0,91; 0,94].
Расширенная серия. JavaScript быстрее в 1,13 раза. JS / Wasm ST: 0,88× [0,88; 0,90].
Dart-код этого тестаKernel(
'allocation_graph',
'Объекты',
'Объектный граф и проход по ссылкам',
'32 768 связанных Cell; allocation включён',
() {
Cell? head;
for (var i = 0; i < 32768; i++) {
head = Cell(data.values[i], head);
}
var sum = 0;
while (head != null) {
sum += head.value;
head = head.next;
}
return sum;
},
)
В приложении несколько классов могут реализовывать один интерфейс: например, разные операции над документом. Вызов идёт через общий тип, а нужная реализация выбирается во время выполнения.
Тест 300 000 раз вызывает apply у трёх чередующихся реализаций. Каждая делает короткую целочисленную операцию. Такой профиль показывает совместную стоимость выбора метода, самого вызова и оптимизации повторяющейся последовательности типов. В одном измерительном пакете задача повторялась 22 раза.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,553 | 0,486 | 0,484 |
Основная серия. Wasm быстрее в 1,13 раза. JS / Wasm ST: 1,13× [1,11; 1,14].
Dart-код этого тестаKernel(
'dispatch',
'Объекты',
'Полиморфные вызовы',
'300 000 вызовов; 3 реализации',
() {
var x = data.seed & 65535;
for (var i = 0; i < 300000; i++) {
x = operations[i % 3].apply(x);
}
return x;
},
)
Сортировка нужна спискам, таблицам и результатам поиска. Для опыта взяты 20 000 целых значений с воспроизводимым содержимым.
В каждом повторе создаётся копия входного списка, затем вызывается sort. После сортировки вычисляется контрольная сумма, зависящая от порядка элементов. Измеряется полный путь подготовки, сортировки и проверки результата. В одном измерительном пакете задача повторялась 7 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 3,441 | 3,679 | 3,738 |
Расширенная серия | 3,308 | 3,650 | 3,670 |
Основная серия. Скорость заметно меняется между запусками. JS / Wasm ST: 0,94× [0,88; 1,40].
Расширенная серия. Разница находится около принятого порога 10%. JS / Wasm ST: 0,91× [0,89; 0,93].
Dart-код этого тестаKernel(
'sort',
'Коллекции',
'Сортировка чисел',
'20 000 значений; копирование включено',
() {
final out = data.values.take(20000).toList()..sort();
var sum = 0;
for (var i = 0; i < out.length; i++) {
sum = (sum + out[i] * (i + 1)) % 1000000007;
}
return sum;
},
)
Типичный пример — найти запись по строковому идентификатору. В Dart для этого часто используется Map.
Словарь и ключи подготовлены заранее. Во время замера выполняются 20 000 поисков в перемешанном порядке. Результаты складываются в сумму. В одном измерительном пакете задача повторялась 77 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,258 | 0,395 | 0,421 |
Основная серия. JavaScript быстрее в 1,54 раза. JS / Wasm ST: 0,65× [0,61; 0,68].
Dart-код этого тестаKernel(
'map',
'Коллекции',
'Хеш-таблица: поиск строковых ключей',
'20 000 поисков; Map подготовлен',
() {
var sum = 0;
for (final k in keys) {
sum += map[k]!;
}
return sum;
},
)
В журнале событий или экспорте приходится собирать строку из множества фрагментов. Для этой задачи используется StringBuffer.
В буфер записываются 2 000 фрагментов с номером и числом, затем получается итоговая строка и её длина. В одном измерительном пакете задача повторялась 313 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,039 | 0,089 | 0,090 |
Основная серия. JavaScript быстрее в 2,30 раза. JS / Wasm ST: 0,43× [0,43; 0,44].
Dart-код этого тестаKernel(
'string_build',
'Текст',
'StringBuffer / форматирование',
'2 000 строк',
() {
final out = StringBuffer();
for (var i = 0; i < 2000; i++) {
out.write('item_$i:${data.values[i]};');
}
return out.toString().length;
},
)
Перед сохранением текста или отправкой по сети строку часто переводят в байты UTF-8. Вход содержит латиницу, кириллицу, японские символы и эмодзи.
Кодируется текст из 2 000 строк. Затем байты суммируются, чтобы результат вычисления использовался. Поэтому время охватывает кодирование и контрольный проход по полученному массиву. В одном измерительном пакете задача повторялась 25 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,661 | 0,216 | 0,215 |
Основная серия. Wasm быстрее в 3,08 раза. JS / Wasm ST: 3,08× [1,86; 4,37].
Dart-код этого тестаKernel(
'utf8_encode',
'Текст',
'UTF-8: кодирование Unicode',
'2 000 многоязычных строк',
() => utf8.encode(data.text).fold<int>(0, (a, b) => a + b),
)
После получения файла или сетевого ответа байты UTF-8 нужно превратить в строку. Здесь используются те же многоязычные данные.
Закодированный массив с 2 000 многоязычных строк подготовлен заранее. Во время замера выполняется utf8.decode и читается длина строки. Направление преобразования существенно: кодирование и декодирование используют разные алгоритмы и могут иметь разных победителей. В одном измерительном пакете задача повторялась 358 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,055 | 0,174 | 0,174 |
Основная серия. JavaScript быстрее в 3,14 раза. JS / Wasm ST: 0,32× [0,32; 0,33].
Dart-код этого тестаKernel(
'utf8_decode',
'Текст',
'UTF-8: декодирование Unicode',
'2 000 многоязычных строк',
() => utf8.decode(encoded).length,
)
Из текста журнала нужно извлечь номера записей вида item_123. Для этого подходит регулярное выражение с группой захвата.
Поиск находит 2 000 совпадений, извлекает цифры, переводит их в int и суммирует. В одном измерительном пакете задача повторялась 114 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,153 | 0,284 | 0,286 |
Основная серия. JavaScript быстрее в 1,85 раза. JS / Wasm ST: 0,54× [0,53; 0,55].
Dart-код этого тестаKernel('regexp', 'Текст', 'RegExp: поиск и группы', '2 000 совпадений', () {
var sum = 0;
for (final m in regular.allMatches(data.text)) {
sum += int.parse(m.group(1)!);
}
return sum;
})
Перед отправкой набора записей в API приложение превращает объекты в JSON. В тесте каждая из 2 000 записей содержит четыре поля: id, name, value и active.
Список объектов существует к началу измерения. jsonEncode создаёт строку, затем читается её длина. Полученный коэффициент относится к подготовке JSON в памяти; в полном пользовательском действии учитывается ещё передача запроса. В одном измерительном пакете задача повторялась 46 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,470 | 0,340 | 0,341 |
Основная серия. Wasm быстрее в 1,38 раза. JS / Wasm ST: 1,38× [1,36; 1,40].
Dart-код этого тестаKernel(
'json_encode',
'Сериализация',
'JSON: encode',
'2 000 объектов × 4 поля',
() => jsonEncode(data.records).length,
)
При открытии списка приложение получает JSON и извлекает нужные поля. Для сравнения используется текст с теми же 2 000 объектами.
jsonDecode создаёт коллекции, после чего код проходит по строкам и складывает поле value. Измерение включает разбор, создание объектов и доступ к полям. Этот путь отличается от подготовки JSON, поэтому результаты двух направлений рассматриваются отдельно. В одном измерительном пакете задача повторялась 87 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,211 | 0,321 | 0,325 |
Основная серия. JavaScript быстрее в 1,53 раза. JS / Wasm ST: 0,65× [0,65; 0,66].
Dart-код этого тестаKernel(
'json_decode',
'Сериализация',
'JSON: decode и чтение поля',
'2 000 объектов × 4 поля',
() {
final rows = jsonDecode(data.json) as List;
var sum = 0;
for (final r in rows) {
sum += (r['value'] as num).toInt();
}
return sum;
},
)
Flutter-код иногда много раз обращается к маленьким функциям JavaScript-библиотеки. Здесь такая функция всего лишь прибавляет единицу.
20 000 вызовов объединены в цикл. V8 может встроить простую функцию и оптимизировать этот цикл целиком. В одном измерительном пакете задача повторялась 228 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,006 | 0,197 | 0,197 |
Основная серия. JavaScript быстрее в 33,52 раза. JS / Wasm ST: 0,03× [0,03; 0,03].
Dart-код этого тестаWorkload('interop_scalar','Граница JS','Dart → JS: маленькие вызовы','20 000 вызовов',sync:(){
var sum=0.0; for(var i=0;i<20000;i++) {sum+=bridge.scalar(i.toDouble());} return sum;
})
Второй вариант интеграции передаёт сразу весь массив одним вызовом. JavaScript получает 65 536 чисел и суммирует их.
Время включает преобразование представления через toJS и работу функции суммирования. Его объём полезной работы отличается от серии маленьких вызовов. В одном измерительном пакете задача повторялась 512 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,079 | 0,155 | 0,155 |
Основная серия. JavaScript быстрее в 1,98 раза. JS / Wasm ST: 0,51× [0,50; 0,52].
Dart-код этого тестаWorkload('interop_batch','Граница JS','Dart → JS: один пакет','65 536 чисел; toJS + sum',sync:()=>bridge.sum(interop.toJS))
Иногда после обработки массива JavaScript-библиотекой нужно снова обратиться к нему из Dart. Здесь проверяется такой круговой путь.
Код получает JavaScript-представление, считает сумму в JavaScript, преобразует представление обратно через toDart и читает последний элемент. Конкретная реализация преобразования зависит от среды и может использовать обёртку или копирование. В одном измерительном пакете задача повторялась 417 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,078 | 0,155 | 0,154 |
Основная серия. JavaScript быстрее в 1,98 раза. JS / Wasm ST: 0,50× [0,50; 0,51].
Dart-код этого тестаWorkload('interop_roundtrip','Граница JS','TypedData: toJS → toDart','65 536 чисел; включая доступ JS',sync:(){
final js=interop.toJS; final sum=bridge.sum(js); return sum+js.toDart.last;
})
Асинхронная функция может выполнять множество коротких продолжений даже при уже готовых данных. Это бывает в цепочках Future и обработчиках событий.
Тест 1 000 раз выполняет await Future.value(i) и суммирует значения. Так измеряется организация асинхронных продолжений. Реальные сетевые операции добавляют собственное время ожидания. В одном измерительном пакете задача повторялась 109 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,103 | 0,057 | 0,056 |
Основная серия. Wasm быстрее в 1,80 раза. JS / Wasm ST: 1,80× [1,77; 1,85].
Dart-код этого тестаWorkload('microtasks','Асинхронность','Future / microtasks','1 000 await Future.value',async:()async{
var sum=0;for(var i=0;i<1000;i++) {sum+=await Future.value(i);}return sum;
})
Для проверки целостности данных браузер умеет вычислять SHA-256 через WebCrypto. Обе сборки передают в него одинаковый буфер 64 КиБ.
Замер включает вызов, ожидание готового хеша и суммирование его байтов. Криптографическое вычисление выполняет браузерная служба. В одном измерительном пакете задача повторялась 512 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,024 | 0,024 | 0,024 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 1,02× [1,01; 1,03].
Dart-код этого тестаfor(final c in const [
['webcrypto','WebCrypto: SHA-256','64 KiB; браузерный native API'],
['timer_wait','Ожидание таймера','15 ms requested; elapsed measured'],
['local_fetch','Локальный HTTP fetch','64 KiB; no-store; без WAN'],
['indexeddb','IndexedDB: put/get/delete','64 KiB; одна транзакция'],
['worker_transfer','Web Worker: transfer + ответ','64 KiB; копирование и суммирование'],
]) Workload(c[0],'Браузер / I/O',c[1],c[2],async:()async=>(await bridge.nativeAsync(c[0].toJS).toDart).toDartDouble),
@JS('bench.nativeAsync') external JSPromise<JSNumber> nativeAsync(JSString id);
case 'webcrypto':return [...new Uint8Array(await crypto.subtle.digest('SHA-256',bench.bytes))].reduce((a,b)=>a+b,0);
Приложение запрашивает задержку, после которой продолжает выполнение. Проверяем обычный таймер на 15 миллисекунд.
Время берётся по фактическому завершению ожидания. Браузер ставит продолжение в очередь, поэтому реальная задержка может быть больше запрошенной. В одном измерительном пакете задача повторялась 2 раза.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 16,766 | 17,044 | 16,672 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 0,98× [0,92; 1,03].
Dart-код этого тестаfor(final c in const [
['webcrypto','WebCrypto: SHA-256','64 KiB; браузерный native API'],
['timer_wait','Ожидание таймера','15 ms requested; elapsed measured'],
['local_fetch','Локальный HTTP fetch','64 KiB; no-store; без WAN'],
['indexeddb','IndexedDB: put/get/delete','64 KiB; одна транзакция'],
['worker_transfer','Web Worker: transfer + ответ','64 KiB; копирование и суммирование'],
]) Workload(c[0],'Браузер / I/O',c[1],c[2],async:()async=>(await bridge.nativeAsync(c[0].toJS).toDart).toDartDouble),
@JS('bench.nativeAsync') external JSPromise<JSNumber> nativeAsync(JSString id);
case 'timer_wait':await new Promise(r=>setTimeout(r,15));return 15;
Для просмотра влияния сборки на получение данных загружается небольшой двоичный файл с локального сервера.
Запрос fetch получает 64 КиБ с отключённым HTTP-кэшированием, преобразует ответ в массив и суммирует байты. Сервер расположен на том же компьютере. В одном измерительном пакете задача повторялась 32 раза.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,565 | 0,523 | 0,498 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 1,07× [1,00; 1,10].
Dart-код этого тестаfor(final c in const [
['webcrypto','WebCrypto: SHA-256','64 KiB; браузерный native API'],
['timer_wait','Ожидание таймера','15 ms requested; elapsed measured'],
['local_fetch','Локальный HTTP fetch','64 KiB; no-store; без WAN'],
['indexeddb','IndexedDB: put/get/delete','64 KiB; одна транзакция'],
['worker_transfer','Web Worker: transfer + ответ','64 KiB; копирование и суммирование'],
]) Workload(c[0],'Браузер / I/O',c[1],c[2],async:()async=>(await bridge.nativeAsync(c[0].toJS).toDart).toDartDouble),
@JS('bench.nativeAsync') external JSPromise<JSNumber> nativeAsync(JSString id);
case 'local_fetch':{const r=await fetch('payload.bin',{cache:'no-store'});if(!r.ok)throw Error('fetch '+r.status);return bench.sum(new Uint8Array(await r.arrayBuffer()));}
Локальный кэш веб-приложения может хранить буферы в IndexedDB. В опыте один буфер записывается, читается и удаляется.
Все действия выполняются в одной транзакции. Таймер останавливается после её завершения; прочитанные байты проверяются суммой. В одном измерительном пакете задача повторялась 73 раза.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,264 | 0,253 | 0,244 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 1,05× [1,02; 1,07].
Dart-код этого тестаfor(final c in const [
['webcrypto','WebCrypto: SHA-256','64 KiB; браузерный native API'],
['timer_wait','Ожидание таймера','15 ms requested; elapsed measured'],
['local_fetch','Локальный HTTP fetch','64 KiB; no-store; без WAN'],
['indexeddb','IndexedDB: put/get/delete','64 KiB; одна транзакция'],
['worker_transfer','Web Worker: transfer + ответ','64 KiB; копирование и суммирование'],
]) Workload(c[0],'Браузер / I/O',c[1],c[2],async:()async=>(await bridge.nativeAsync(c[0].toJS).toDart).toDartDouble),
@JS('bench.nativeAsync') external JSPromise<JSNumber> nativeAsync(JSString id);
case 'indexeddb':return await new Promise((resolve,reject)=>{const tx=bench.db.transaction('scratch','readwrite');const store=tx.objectStore('scratch');
store.put(bench.bytes,'sample');let sum;store.get('sample').onsuccess=e=>{sum=bench.sum(e.target.result);};store.delete('sample');
tx.oncomplete=()=>resolve(sum);tx.>reject(tx.error);tx.onabort=()=>reject(tx.error);});
Фоновый Web Worker позволяет вынести обработку данных из основного потока. Здесь ему передаётся буфер, а обратно возвращается сумма байтов.
Сначала создаётся копия 64 КиБ, затем её ArrayBuffer передаётся с передачей владения. Worker суммирует данные и отвечает. Полное время включает подготовку, обмен сообщениями, очередь фонового потока и вычисление. В одном измерительном пакете задача повторялась 76 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,046 | 0,045 | 0,045 |
Расширенная серия | 0,046 | 0,048 | 0,048 |
Основная серия. Разница находится около принятого порога 10%. JS / Wasm ST: 0,99× [0,95; 1,16].
Расширенная серия. Скорость практически одинакова. JS / Wasm ST: 0,96× [0,93; 0,97].
Dart-код этого тестаfor(final c in const [
['webcrypto','WebCrypto: SHA-256','64 KiB; браузерный native API'],
['timer_wait','Ожидание таймера','15 ms requested; elapsed measured'],
['local_fetch','Локальный HTTP fetch','64 KiB; no-store; без WAN'],
['indexeddb','IndexedDB: put/get/delete','64 KiB; одна транзакция'],
['worker_transfer','Web Worker: transfer + ответ','64 KiB; копирование и суммирование'],
]) Workload(c[0],'Браузер / I/O',c[1],c[2],async:()async=>(await bridge.nativeAsync(c[0].toJS).toDart).toDartDouble),
@JS('bench.nativeAsync') external JSPromise<JSNumber> nativeAsync(JSString id);
case 'worker_transfer':return await new Promise((resolve,reject)=>{const copy=bench.bytes.slice().buffer;bench.worker.onmessage=e=>resolve(e.data);bench.worker.
onmessage = ({data}) => {
let sum=0; for(const v of new Uint32Array(data)) sum+=v;
postMessage(sum);
};
При работе с обычной HTML-страницей изменение размеров элемента может потребовать пересчёта расположения. Этот эффект проверяется отдельной задачей.
JavaScript-помощник 100 раз меняет ширину элемента и читает offsetWidth. Чтение заставляет браузер завершить пересчёт. В обеих сборках работает один помощник, поэтому большая часть затрат относится к браузерному механизму раскладки. В одном измерительном пакете задача повторялась 46 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,576 | 0,553 | 0,549 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 1,04× [1,00; 1,06].
Dart-код этого тестаWorkload('dom_layout','Браузер / I/O','DOM: запись стиля + forced layout','100 layout flushes; общий JS helper',sync:()=>bridge.nativeSync('dom_layout'.toJS))
case 'dom_layout':{let sum=0;for(let i=0;i<100;i++){bench.dom.style.width=`${200+(i%17)}px`;sum+=bench.dom.offsetWidth;}return sum;}
В текстовом редакторе или списке сообщений Flutter вычисляет размеры абзацев и переносы строк. Это заметная часть работы сложных экранов.
Создаются 100 уникальных абзацев Roboto размером 14 пикселей и шириной 300 пикселей. Каждый Paragraph проходит layout, его высота добавляется к контрольному результату, затем объект освобождается. Влияние компилятора здесь сочетается с работой графического движка. В одном измерительном пакете задача повторялась 6 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 3,473 | 3,226 | 3,194 |
Расширенная серия | 3,318 | 3,121 | 3,098 |
Основная серия. Разница находится около принятого порога 10%. JS / Wasm ST: 1,08× [1,05; 1,13].
Расширенная серия. Скорость практически одинакова. JS / Wasm ST: 1,06× [1,05; 1,07].
В одном запуске изменились размеры абзацев. Я повторил этот блок во всех трёх режимах и включил в таблицу результаты с одинаковой раскладкой текста.
Dart-код этого тестаWorkload('text_layout','Flutter', 'Paragraph: формирование и layout','100 уникальных абзацев',sync:(){
var height=0.0;
for(var i=0;i<100;i++) {
final builder=ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily:'Roboto',fontSize:14))
..addText('Paragraph $i: Flutter text layout and measurement. ' * 3);
final p=builder.build()..layout(const ui.ParagraphConstraints(width:300));height+=p.height;p.dispose();
}return height;
})
Представим плотную панель из 600 небольших ячеек с текстом и меняющимся цветом. При каждом обновлении Flutter заново готовит виджеты и сцену.
Тест выполняет подготовительный кадр и 24 обновления. Каждое обновление ждёт endOfFrame. Поэтому отдельно измерены длительность всей серии и работа внутри кадра. Именно здесь особенно хорошо виден запас времени, который появляется после ускорения подготовки сцены. В одном замере выполнялась одна такая серия.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 427,875 | 410,513 | 410,495 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 1,04× [1,04; 1,05].
Подготовка одного кадра: JavaScript — 15,31 мс, Wasm ST — 7,92 мс, Wasm MT — 7,94 мс.
Dart-код этого тестаFuture<num> runScene(String id)async{
setState((){scene=id;tick=0;});
await SchedulerBinding.instance.endOfFrame;
num checksum=0;
for(var i=0;i<uiFrames;i++){
setState(()=>tick=i+1);
if(id=='ui_scroll'&&scroll.hasClients){scroll.jumpTo((i*127.0)%scroll.position.maxScrollExtent);}
await SchedulerBinding.instance.endOfFrame;
checksum+=i+1;
}
return checksum;
}
Widget sceneWidget(){
switch(scene){
case 'ui_widgets':return Wrap(children:List.generate(600,(i)=>SizedBox(width:34,height:20,
child:ColoredBox(color:Color(0xff17304b+((i+tick)%12)*512),child:Text('${(i+tick)%99}',style:const TextStyle(fontSize:10))))));
case 'ui_scroll':return ListView.builder(controller:scroll,itemCount:50000,itemExtent:42,
itemBuilder:(context,i)=>ListTile(dense:true,leading:const Icon(Icons.data_object),title:Text('Record $i'),trailing:Text('${i*17}')));
case 'ui_paint':case 'ui_layers':return CustomPaint(painter:StressPainter(tick,scene=='ui_layers'),size:Size.infinite);
default:return Center(child:Column(mainAxisSize:MainAxisSize.min,children:[
const Icon(Icons.science_outlined,size:48,color:Color(0xff38bdf8)),const SizedBox(height:12),
Text('${cases.length} задач · 3 режима',style:Theme.of(context).textTheme.titleLarge),
const SizedBox(height:8),const Text('Вычисления · память · браузер · Flutter'),
]));
}
}
В длинном списке важно, сколько работы требуется при смене видимой области. Используется ListView.builder с 50 000 строками фиксированной высоты.
Позиция прокрутки меняется 24 раза по одинаковой формуле через jumpTo. Flutter создаёт и раскладывает элементы видимой области. Такой сценарий воспроизводит смену содержимого при прокрутке, а характер движения определяется заданными программными шагами. В одном замере выполнялась одна такая серия.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 410,195 | 409,913 | 410,355 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 1,00× [1,00; 1,00].
Подготовка одного кадра: JavaScript — 5,33 мс, Wasm ST — 3,23 мс, Wasm MT — 3,17 мс.
Dart-код этого тестаFuture<num> runScene(String id)async{
setState((){scene=id;tick=0;});
await SchedulerBinding.instance.endOfFrame;
num checksum=0;
for(var i=0;i<uiFrames;i++){
setState(()=>tick=i+1);
if(id=='ui_scroll'&&scroll.hasClients){scroll.jumpTo((i*127.0)%scroll.position.maxScrollExtent);}
await SchedulerBinding.instance.endOfFrame;
checksum+=i+1;
}
return checksum;
}
Widget sceneWidget(){
switch(scene){
case 'ui_widgets':return Wrap(children:List.generate(600,(i)=>SizedBox(width:34,height:20,
child:ColoredBox(color:Color(0xff17304b+((i+tick)%12)*512),child:Text('${(i+tick)%99}',style:const TextStyle(fontSize:10))))));
case 'ui_scroll':return ListView.builder(controller:scroll,itemCount:50000,itemExtent:42,
itemBuilder:(context,i)=>ListTile(dense:true,leading:const Icon(Icons.data_object),title:Text('Record $i'),trailing:Text('${i*17}')));
case 'ui_paint':case 'ui_layers':return CustomPaint(painter:StressPainter(tick,scene=='ui_layers'),size:Size.infinite);
default:return Center(child:Column(mainAxisSize:MainAxisSize.min,children:[
const Icon(Icons.science_outlined,size:48,color:Color(0xff38bdf8)),const SizedBox(height:12),
Text('${cases.length} задач · 3 режима',style:Theme.of(context).textTheme.titleLarge),
const SizedBox(height:8),const Text('Вычисления · память · браузер · Flutter'),
]));
}
}
Диаграмма или визуализация может рисовать тысячи простых фигур каждый кадр. Для такого случая выбран CustomPainter с 2 000 кругами.
В течение 24 кадров меняются координаты фигур. Замер включает подготовку команды рисования и ожидание кадров; показатели FrameTiming отдельно показывают этапы Flutter. Итог зависит одновременно от Dart-кода, рендерера и расписания браузера. В одном замере выполнялась одна такая серия.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 410,127 | 410,448 | 410,475 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 1,00× [1,00; 1,00].
Подготовка одного кадра: JavaScript — 5,13 мс, Wasm ST — 2,00 мс, Wasm MT — 2,03 мс.
Dart-код этого тестаFuture<num> runScene(String id)async{
setState((){scene=id;tick=0;});
await SchedulerBinding.instance.endOfFrame;
num checksum=0;
for(var i=0;i<uiFrames;i++){
setState(()=>tick=i+1);
if(id=='ui_scroll'&&scroll.hasClients){scroll.jumpTo((i*127.0)%scroll.position.maxScrollExtent);}
await SchedulerBinding.instance.endOfFrame;
checksum+=i+1;
}
return checksum;
}
Widget sceneWidget(){
switch(scene){
case 'ui_widgets':return Wrap(children:List.generate(600,(i)=>SizedBox(width:34,height:20,
child:ColoredBox(color:Color(0xff17304b+((i+tick)%12)*512),child:Text('${(i+tick)%99}',style:const TextStyle(fontSize:10))))));
case 'ui_scroll':return ListView.builder(controller:scroll,itemCount:50000,itemExtent:42,
itemBuilder:(context,i)=>ListTile(dense:true,leading:const Icon(Icons.data_object),title:Text('Record $i'),trailing:Text('${i*17}')));
case 'ui_paint':case 'ui_layers':return CustomPaint(painter:StressPainter(tick,scene=='ui_layers'),size:Size.infinite);
default:return Center(child:Column(mainAxisSize:MainAxisSize.min,children:[
const Icon(Icons.science_outlined,size:48,color:Color(0xff38bdf8)),const SizedBox(height:12),
Text('${cases.length} задач · 3 режима',style:Theme.of(context).textTheme.titleLarge),
const SizedBox(height:8),const Text('Вычисления · память · браузер · Flutter'),
]));
}
}
class StressPainter extends CustomPainter {
StressPainter(this.tick,this.layers);final int tick;final bool layers;
@override void paint(Canvas canvas,Size size){
final paint=Paint();
if(layers){
for(var i=0;i<100;i++){
final x=(i*31+tick*2)%size.width,y=(i*17)%size.height;
final r=Rect.fromLTWH(x,y,72,60);canvas.saveLayer(r,Paint()..color=const Color(0xaaeeeeee));
canvas.clipRRect(RRect.fromRectAndRadius(r,const Radius.circular(14)));
canvas.drawRect(r,paint..color=const Color(0xff38bdf8));
canvas.drawCircle(Offset(x+24,y+24),22,paint..color=const Color(0xffa78bfa));canvas.restore();
}
}else{
for(var i=0;i<2000;i++){
paint.color=Color.fromARGB(160,(i*17)%256,(i*31)%256,190);
canvas.drawCircle(Offset((i*37+tick*3)%size.width,(i*19)%size.height),3+(i%7).toDouble(),paint);
}
}
}
@override bool shouldRepaint(StressPainter old)=>old.tick!=tick||old.layers!=layers;
}
Полупрозрачность, скруглённая обрезка и промежуточные слои усложняют отрисовку. Здесь эти операции повторяются для 100 элементов в каждом кадре.
Каждый элемент вызывает saveLayer, clipRRect, рисование прямоугольника и круга, затем restore. При сравнении JavaScript и Wasm одновременно меняются компилятор Dart и используемый Flutter-рендерер. В одном замере выполнялась одна такая серия.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 410,485 | 410,653 | 410,580 |
Основная серия. Скорость практически одинакова. JS / Wasm ST: 1,00× [1,00; 1,00].
Подготовка одного кадра: JavaScript — 3,41 мс, Wasm ST — 1,76 мс, Wasm MT — 1,69 мс.
Dart-код этого тестаFuture<num> runScene(String id)async{
setState((){scene=id;tick=0;});
await SchedulerBinding.instance.endOfFrame;
num checksum=0;
for(var i=0;i<uiFrames;i++){
setState(()=>tick=i+1);
if(id=='ui_scroll'&&scroll.hasClients){scroll.jumpTo((i*127.0)%scroll.position.maxScrollExtent);}
await SchedulerBinding.instance.endOfFrame;
checksum+=i+1;
}
return checksum;
}
Widget sceneWidget(){
switch(scene){
case 'ui_widgets':return Wrap(children:List.generate(600,(i)=>SizedBox(width:34,height:20,
child:ColoredBox(color:Color(0xff17304b+((i+tick)%12)*512),child:Text('${(i+tick)%99}',style:const TextStyle(fontSize:10))))));
case 'ui_scroll':return ListView.builder(controller:scroll,itemCount:50000,itemExtent:42,
itemBuilder:(context,i)=>ListTile(dense:true,leading:const Icon(Icons.data_object),title:Text('Record $i'),trailing:Text('${i*17}')));
case 'ui_paint':case 'ui_layers':return CustomPaint(painter:StressPainter(tick,scene=='ui_layers'),size:Size.infinite);
default:return Center(child:Column(mainAxisSize:MainAxisSize.min,children:[
const Icon(Icons.science_outlined,size:48,color:Color(0xff38bdf8)),const SizedBox(height:12),
Text('${cases.length} задач · 3 режима',style:Theme.of(context).textTheme.titleLarge),
const SizedBox(height:8),const Text('Вычисления · память · браузер · Flutter'),
]));
}
}
class StressPainter extends CustomPainter {
StressPainter(this.tick,this.layers);final int tick;final bool layers;
@override void paint(Canvas canvas,Size size){
final paint=Paint();
if(layers){
for(var i=0;i<100;i++){
final x=(i*31+tick*2)%size.width,y=(i*17)%size.height;
final r=Rect.fromLTWH(x,y,72,60);canvas.saveLayer(r,Paint()..color=const Color(0xaaeeeeee));
canvas.clipRRect(RRect.fromRectAndRadius(r,const Radius.circular(14)));
canvas.drawRect(r,paint..color=const Color(0xff38bdf8));
canvas.drawCircle(Offset(x+24,y+24),22,paint..color=const Color(0xffa78bfa));canvas.restore();
}
}else{
for(var i=0;i<2000;i++){
paint.color=Color.fromARGB(160,(i*17)%256,(i*31)%256,190);
canvas.drawCircle(Offset((i*37+tick*3)%size.width,(i*19)%size.height),3+(i%7).toDouble(),paint);
}
}
}
@override bool shouldRepaint(StressPainter old)=>old.tick!=tick||old.layers!=layers;
}
Следующие пять задач сравнивают специально написанные вычислительные модули. Dart организует запуск через JavaScript interop. В базовой JavaScript-сборке поочерёдно вызываются ручная JavaScript-функция, обычный Wasm-модуль и SIMD-вариант. Подготовка входной памяти выполнена заранее.
42. Суммирование массива в отдельной JavaScript-функцииЧтобы увидеть стоимость отдельного вычислительного ядра, сначала измеряем ручную JavaScript-функцию суммирования.
Функция читает 262 144 целых значения из линейной памяти Wasm через Uint32Array. Данные общие с двумя следующими вариантами. Полученное время служит базой сравнения скалярного и SIMD-модулей. В одном измерительном пакете задача повторялась 114 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,261 | 0,260 | 0,259 |
Это исходная JavaScript-реализация для сопоставления с Wasm-модулем. Три времени выше показывают её вызов из разных сборок Flutter.
Dart-код этого тестаfor(final c in const [
['external_js_sum','Сумма: написанный вручную JS','262 144 uint32; 1 MiB'],
['external_wasm_sum','Сумма: скалярный Wasm','тот же массив, точный i32 result'],
['external_simd_sum','Сумма: явный Wasm SIMD','i32x4.add; тот же массив'],
['external_js_i64','Точные 64 бита: JS BigInt','10 000 multiply/xor modulo 2^64'],
['external_wasm_i64','Точные 64 бита: Wasm i64','тот же алгоритм и точный результат'],
]) Workload(c[0],'Контрольные Wasm-модули',c[1],c[2],sync:()=>bridge.nativeSync(c[0].toJS)),
@JS('bench.nativeSync') external double nativeSync(JSString id);
sum:a=>{let s=0;for(let i=0;i<a.length;i++)s+=a[i];return s;},
case 'external_js_sum':return bench.sum(bench.ints);
Теперь ту же сумму вычисляет небольшой модуль WebAssembly. На каждом шаге он загружает одно число и прибавляет его к накопленной сумме.
Объём и содержимое памяти совпадают с JavaScript-вариантом. Все значения выбраны так, чтобы итоговая сумма помещалась в используемый диапазон. В одном измерительном пакете задача повторялась 218 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,070 | 0,069 | 0,069 |
Wasm быстрее в 3,76 раза. Сравниваются модули внутри JavaScript-сборки. Отношение времени и 95%-й интервал: 3,76× [2,22; 4,27].
Dart-код этого тестаfor(final c in const [
['external_js_sum','Сумма: написанный вручную JS','262 144 uint32; 1 MiB'],
['external_wasm_sum','Сумма: скалярный Wasm','тот же массив, точный i32 result'],
['external_simd_sum','Сумма: явный Wasm SIMD','i32x4.add; тот же массив'],
['external_js_i64','Точные 64 бита: JS BigInt','10 000 multiply/xor modulo 2^64'],
['external_wasm_i64','Точные 64 бита: Wasm i64','тот же алгоритм и точный результат'],
]) Workload(c[0],'Контрольные Wasm-модули',c[1],c[2],sync:()=>bridge.nativeSync(c[0].toJS)),
@JS('bench.nativeSync') external double nativeSync(JSString id);
case 'external_wasm_sum':return bench.wasm.sum(bench.ints.length);
(module
(memory (export "memory") 64)
(func (export "sum") (param $n i32) (result i32)
(local $p i32) (local $s i32)
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $p) (local.get $n)))
(local.set $s (i32.add (local.get $s) (i32.load (i32.shl (local.get $p) (i32.const 2)))))
(local.set $p (i32.add (local.get $p) (i32.const 1)))
(br $loop))) (local.get $s))
;; n must be a multiple of four. Tests cover n=0,4,8 and full input.
(func (export "simd") (param $n i32) (result i32)
(local $p i32) (local $s v128)
(local.set $s (v128.const i32x4 0 0 0 0))
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $p) (local.get $n)))
(local.set $s (i32x4.add (local.get $s) (v128.load (i32.shl (local.get $p) (i32.const 2)))))
(local.set $p (i32.add (local.get $p) (i32.const 4)))
(br $loop)))
(i32.add (i32.add (i32x4.extract_lane 0 (local.get $s)) (i32x4.extract_lane 1 (local.get $s)))
(i32.add (i32x4.extract_lane 2 (local.get $s)) (i32x4.extract_lane 3 (local.get $s)))))
(func (export "i64mix") (param $n i32) (result i32)
(local $i i32) (local $x i64)
(local.set $x (i64.const 1234567))
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $i) (local.get $n)))
(local.set $x (i64.xor (i64.mul (local.get $x) (i64.const 6364136223846793005)) (i64.extend_i32_u (local.get $i))))
(local.set $x (i64.xor (local.get $x) (i64.shr_u (local.get $x) (i64.const 33))))
(local.set $i (i32.add (local.get $i) (i32.const 1))) (br $loop)))
(i32.and (i32.wrap_i64 (local.get $x)) (i32.const 2147483647)))
)
Третий вариант складывает числа группами по четыре с помощью SIMD. Процессор выполняет несколько одинаковых операций одной инструкцией.
Модуль загружает вектор v128, применяет i32x4.add и в конце объединяет четыре компонента. Входная длина кратна четырём. В одном измерительном пакете задача повторялась 157 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,031 | 0,032 | 0,031 |
Wasm SIMD быстрее в 8,46 раза. Сравниваются модули внутри JavaScript-сборки. Отношение времени и 95%-й интервал: 8,46× [5,08; 9,47].
Dart-код этого тестаfor(final c in const [
['external_js_sum','Сумма: написанный вручную JS','262 144 uint32; 1 MiB'],
['external_wasm_sum','Сумма: скалярный Wasm','тот же массив, точный i32 result'],
['external_simd_sum','Сумма: явный Wasm SIMD','i32x4.add; тот же массив'],
['external_js_i64','Точные 64 бита: JS BigInt','10 000 multiply/xor modulo 2^64'],
['external_wasm_i64','Точные 64 бита: Wasm i64','тот же алгоритм и точный результат'],
]) Workload(c[0],'Контрольные Wasm-модули',c[1],c[2],sync:()=>bridge.nativeSync(c[0].toJS)),
@JS('bench.nativeSync') external double nativeSync(JSString id);
case 'external_simd_sum':return bench.wasm.simd(bench.ints.length);
(module
(memory (export "memory") 64)
(func (export "sum") (param $n i32) (result i32)
(local $p i32) (local $s i32)
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $p) (local.get $n)))
(local.set $s (i32.add (local.get $s) (i32.load (i32.shl (local.get $p) (i32.const 2)))))
(local.set $p (i32.add (local.get $p) (i32.const 1)))
(br $loop))) (local.get $s))
;; n must be a multiple of four. Tests cover n=0,4,8 and full input.
(func (export "simd") (param $n i32) (result i32)
(local $p i32) (local $s v128)
(local.set $s (v128.const i32x4 0 0 0 0))
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $p) (local.get $n)))
(local.set $s (i32x4.add (local.get $s) (v128.load (i32.shl (local.get $p) (i32.const 2)))))
(local.set $p (i32.add (local.get $p) (i32.const 4)))
(br $loop)))
(i32.add (i32.add (i32x4.extract_lane 0 (local.get $s)) (i32x4.extract_lane 1 (local.get $s)))
(i32.add (i32x4.extract_lane 2 (local.get $s)) (i32x4.extract_lane 3 (local.get $s)))))
(func (export "i64mix") (param $n i32) (result i32)
(local $i i32) (local $x i64)
(local.set $x (i64.const 1234567))
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $i) (local.get $n)))
(local.set $x (i64.xor (i64.mul (local.get $x) (i64.const 6364136223846793005)) (i64.extend_i32_u (local.get $i))))
(local.set $x (i64.xor (local.get $x) (i64.shr_u (local.get $x) (i64.const 33))))
(local.set $i (i32.add (local.get $i) (i32.const 1))) (br $loop)))
(i32.and (i32.wrap_i64 (local.get $x)) (i32.const 2147483647)))
)
Для точных 64-битных вычислений в JavaScript используется BigInt. В этой задаче значения многократно умножаются и перемешиваются.
На каждом из 10 000 шагов сохраняются младшие 64 бита, затем выполняется XOR со сдвинутым значением. Старшие разряды влияют на последующие шаги. Это базовая реализация для следующего Wasm-модуля. В одном измерительном пакете задача повторялась 112 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,181 | 0,179 | 0,180 |
Это исходная JavaScript-реализация для сопоставления с Wasm-модулем. Три времени выше показывают её вызов из разных сборок Flutter.
Dart-код этого тестаfor(final c in const [
['external_js_sum','Сумма: написанный вручную JS','262 144 uint32; 1 MiB'],
['external_wasm_sum','Сумма: скалярный Wasm','тот же массив, точный i32 result'],
['external_simd_sum','Сумма: явный Wasm SIMD','i32x4.add; тот же массив'],
['external_js_i64','Точные 64 бита: JS BigInt','10 000 multiply/xor modulo 2^64'],
['external_wasm_i64','Точные 64 бита: Wasm i64','тот же алгоритм и точный результат'],
]) Workload(c[0],'Контрольные Wasm-модули',c[1],c[2],sync:()=>bridge.nativeSync(c[0].toJS)),
@JS('bench.nativeSync') external double nativeSync(JSString id);
bench.i64js=n=>{let x=1234567n;for(let i=0;i<n;i++){x=BigInt.asUintN(64,x*6364136223846793005n^BigInt(i));x^=x>>33n;}return Number(x&2147483647n);};
case 'external_js_i64':return bench.i64js(10000);
WebAssembly имеет встроенные инструкции для 64-битных целых. Они позволяют выразить тот же алгоритм непосредственно машинными операциями.
Последовательность действий и контрольные ответы совпадают с JavaScript BigInt. Разница времени описывает стоимость двух способов выполнения точной 64-битной арифметики. Для произвольной длины числа, как в 127-битном тесте Dart BigInt, нужен другой механизм. В одном измерительном пакете задача повторялась 512 раз.
Серия | JavaScript, мс | Wasm ST, мс | Wasm MT, мс |
|---|---|---|---|
Основная серия | 0,014 | 0,014 | 0,014 |
Wasm быстрее в 13,07 раза. Сравниваются модули внутри JavaScript-сборки. Отношение времени и 95%-й интервал: 13,07× [12,93; 21,78].
Dart-код этого тестаfor(final c in const [
['external_js_sum','Сумма: написанный вручную JS','262 144 uint32; 1 MiB'],
['external_wasm_sum','Сумма: скалярный Wasm','тот же массив, точный i32 result'],
['external_simd_sum','Сумма: явный Wasm SIMD','i32x4.add; тот же массив'],
['external_js_i64','Точные 64 бита: JS BigInt','10 000 multiply/xor modulo 2^64'],
['external_wasm_i64','Точные 64 бита: Wasm i64','тот же алгоритм и точный результат'],
]) Workload(c[0],'Контрольные Wasm-модули',c[1],c[2],sync:()=>bridge.nativeSync(c[0].toJS)),
@JS('bench.nativeSync') external double nativeSync(JSString id);
case 'external_wasm_i64':return bench.wasm.i64mix(10000);
(module
(memory (export "memory") 64)
(func (export "sum") (param $n i32) (result i32)
(local $p i32) (local $s i32)
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $p) (local.get $n)))
(local.set $s (i32.add (local.get $s) (i32.load (i32.shl (local.get $p) (i32.const 2)))))
(local.set $p (i32.add (local.get $p) (i32.const 1)))
(br $loop))) (local.get $s))
;; n must be a multiple of four. Tests cover n=0,4,8 and full input.
(func (export "simd") (param $n i32) (result i32)
(local $p i32) (local $s v128)
(local.set $s (v128.const i32x4 0 0 0 0))
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $p) (local.get $n)))
(local.set $s (i32x4.add (local.get $s) (v128.load (i32.shl (local.get $p) (i32.const 2)))))
(local.set $p (i32.add (local.get $p) (i32.const 4)))
(br $loop)))
(i32.add (i32.add (i32x4.extract_lane 0 (local.get $s)) (i32x4.extract_lane 1 (local.get $s)))
(i32.add (i32x4.extract_lane 2 (local.get $s)) (i32x4.extract_lane 3 (local.get $s)))))
(func (export "i64mix") (param $n i32) (result i32)
(local $i i32) (local $x i64)
(local.set $x (i64.const 1234567))
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $i) (local.get $n)))
(local.set $x (i64.xor (i64.mul (local.get $x) (i64.const 6364136223846793005)) (i64.extend_i32_u (local.get $i))))
(local.set $x (i64.xor (local.get $x) (i64.shr_u (local.get $x) (i64.const 33))))
(local.set $i (i32.add (local.get $i) (i32.const 1))) (br $loop)))
(i32.and (i32.wrap_i64 (local.get $x)) (i32.const 2147483647)))
)
В сценариях интерфейса я измерял две вещи: сколько длится вся серия из 24 обновлений после подготовительного кадра и сколько работы Flutter выполняет внутри каждого кадра. Между обновлениями код ждёт endOfFrame. Из-за этого длительность серии чувствительна к расписанию кадров браузера.
Подготовка кадра (build) охватывает работу Flutter с виджетами, расположением элементов и формированием сцены. Растеризация (raster) — этап обработки сцены и отправки графических команд, который фиксирует движок Flutter. Аппаратное время GPU и физический показ пикселей требуют отдельной инструментальной трассировки.
В таблице приведены медианы и p95. Последний показатель означает, что 95% измеренных значений уложились в это время. Интервалы дольше 25 мс используются как диагностический признак задержек между кадрами.
Сценарий и режим | Подготовка кадра, мс | Подготовка: p95, мс | Растеризация, мс | Растеризация: p95, мс | Интервал кадров: p95, мс | Интервалы дольше 25 мс |
|---|---|---|---|---|---|---|
Прокрутка большого спискаJavaScript · 1050 кадров | 5,332 | 6,448 | 0,416 | 0,540 | 18,563 | 0,0% |
Прокрутка большого спискаWasm · один поток отрисовки · 1050 кадров | 3,226 | 5,262 | 0,412 | 0,627 | 18,538 | 0,0% |
Прокрутка большого спискаWasm · отдельный поток отрисовки · 1050 кадров | 3,170 | 5,074 | 0,625 | 1,203 | 18,570 | 0,0% |
Рисование множества фигурJavaScript · 1050 кадров | 5,133 | 5,567 | 1,585 | 1,735 | 18,555 | 0,0% |
Рисование множества фигурWasm · один поток отрисовки · 1050 кадров | 2,000 | 2,674 | 2,650 | 3,019 | 18,523 | 0,0% |
Рисование множества фигурWasm · отдельный поток отрисовки · 1050 кадров | 2,025 | 2,468 | 2,812 | 3,300 | 18,618 | 0,0% |
Отрисовка с обрезкой и промежуточными слоямиJavaScript · 1050 кадров | 3,413 | 3,930 | 1,246 | 1,456 | 18,580 | 0,0% |
Отрисовка с обрезкой и промежуточными слоямиWasm · один поток отрисовки · 1050 кадров | 1,756 | 2,880 | 2,248 | 2,655 | 18,546 | 0,0% |
Отрисовка с обрезкой и промежуточными слоямиWasm · отдельный поток отрисовки · 1050 кадров | 1,688 | 2,328 | 2,413 | 3,036 | 18,570 | 0,0% |
Обновление экрана с 600 ячейкамиJavaScript · 1050 кадров | 15,310 | 16,621 | 1,040 | 1,205 | 18,036 | 0,0% |
Обновление экрана с 600 ячейкамиWasm · один поток отрисовки · 1050 кадров | 7,920 | 8,805 | 1,208 | 1,362 | 18,559 | 0,0% |
Обновление экрана с 600 ячейкамиWasm · отдельный поток отрисовки · 1050 кадров | 7,943 | 8,583 | 1,370 | 1,797 | 18,575 | 0,0% |
Все четыре сценария показали близкую общую длительность в JavaScript и Wasm ST. При этом подготовка кадров у Wasm занимала меньше времени. Такой запас полезен для усложнения виджетов, дополнительной обработки данных и уменьшения риска задержек при более тяжёлой нагрузке.
Wasm MT также дал близкую длительность серий к Wasm ST. Измеряемый этап растеризации в сценариях списка и 600 ячеек стал дольше. Это отражает затраты организации работы рендерера; оценка видимого эффекта учитывает и всю серию, и интервалы между кадрами.
Все отношения времени кадров и доверительные интервалыJS / ST и JS / MT сравнивают подготовку кадра. В столбцах ST / MT сопоставлены два режима Wasm. Значение выше единицы означает, что режим в знаменателе выполнил этап быстрее.
Сценарий | Подготовка: JS / ST | Подготовка: JS / MT | Вся серия: ST / MT | Растеризация: ST / MT |
|---|---|---|---|---|
Прокрутка большого списка | 1,65× [1,60; 1,73] | 1,69× [1,66; 1,75] | 1,00× [1,00; 1,00] | 0,66× [0,63; 0,72] |
Рисование множества фигур | 2,56× [2,43; 2,61] | 2,54× [2,44; 2,60] | 1,00× [1,00; 1,00] | 0,94× [0,93; 0,97] |
Отрисовка с обрезкой и промежуточными слоями | 1,94× [1,87; 2,10] | 2,02× [1,97; 2,09] | 1,00× [1,00; 1,00] | 0,92× [0,91; 0,96] |
Обновление экрана с 600 ячейками | 1,93× [1,90; 1,96] | 1,91× [1,87; 1,99] | 1,00× [1,00; 1,00] | 0,88× [0,87; 0,90] |
Векторный код быстрее работал после перехода с JavaScript на Wasm. Внутри самой Wasm-сборки обычный цикл суммирования оказался быстрее Int32x4. Выбор структуры данных и способа доступа способен заметно повлиять на конечный результат.
Режим | Обычный цикл, мс | Dart Int32x4, мс | Время цикла / Int32x4 · интервал 95% |
|---|---|---|---|
JavaScript | 0,100 | 0,349 | 0,28× [0,28; 0,30] |
Wasm · один поток отрисовки | 0,108 | 0,190 | 0,57× [0,54; 0,58] |
Wasm · отдельный поток отрисовки | 0,108 | 0,187 | 0,58× [0,54; 0,58] |
Установленный Dart 3.13.1 использует NaiveInt32x4List для хранения списка, а I32x4.operator + обращается к WasmI32x4. Время всей операции складывается из доступа к данным, представления значений, проверок и арифметики. Эти отдельные составляющие в данном опыте измерены совместно.
Инструмент WABT 1.0.39 успешно прочитал специальный контрольный модуль и подтвердил SIMD-инструкцию i32x4.add. На GC-типах полного Flutter-приложения разбор остановился. Вывод о реализации Dart опирается на исходники установленного SDK и фактические замеры.
Сколько занимает открытие приложенияВремя запуска измерено до первого завершённого кадра Flutter и подготовки тестовых данных. Первое открытие выполнялось в новом контексте Chrome с пустым HTTP-кэшем. При повторном открытии использовался тот же контекст. Браузерный процесс, ОС и её файловый кэш сохранялись.
Режим | Первое открытие, мс | Повторное открытие, мс | Переданные ресурсы при первом открытии, МиБ |
|---|---|---|---|
JavaScript | 337,650 | 145,845 | 7,56 |
Wasm · один поток отрисовки | 225,775 | 96,325 | 5,43 |
Wasm · отдельный поток отрисовки | 181,795 | 63,278 | 5,43 |
На этом локальном стенде быстрее открывались Wasm-сборки. Ресурсы передавались с того же компьютера в исходном виде, без сжатия. Поэтому таблица описывает локальный запуск при указанных условиях. Публичный сайт добавляет задержки сети и свои настройки доставки ресурсов.
Программный маркер готовности ставится после первого endOfFrame и подготовки данных. Сумма переданных ресурсов рассчитана по Resource Timing; охват заголовков, основного документа и загрузок рабочих потоков определяется этим API. Замеры запуска относятся к основной серии, до последнего уточнения 64-битного контрольного модуля.
Размеры окончательных файлов сборки и их контрольные суммыФайл приложения | Размер, байт | Контрольная сумма SHA-256 |
|---|---|---|
site/js/main.dart.js | 2,211,934 |
|
site/wasm/main.dart.wasm | 1,802,312 |
|
site/wasm/main.dart.mjs | 30,966 |
|
Для обычного интерфейса Wasm даёт прежде всего запас времени внутри кадра. На нашем экране с 600 ячейками подготовка сцены ускорилась почти вдвое, а длительность серии сократилась примерно на четыре процента. Такой результат закономерен для сценария, который ждёт следующий кадр браузера.
Для вычислений важны операция и форма данных. Wasm хорошо показал себя в целочисленных операциях, рекурсии и фильтре изображения. JavaScript выиграл обычные циклы с дробными числами и часть обработки текста. Даже близкие операции — кодирование и декодирование — дали разные результаты.
Крупные ускорения получены в специально организованных модулях. Суммирование с явным SIMD выиграло у ручного JavaScript-цикла в 8,46 раза, а точные 64-битные вычисления — в 13,07 раза. Эти реализации используют конкретные машинные возможности Wasm. Полный путь приложения добавляет подготовку и передачу данных.
Частые короткие вызовы JavaScript требуют внимания к организации кода. В тестах interop быстрее работала JavaScript-сборка. Передача массива за один вызов позволяет объединить полезную работу, а её результат зависит от размера массива и стоимости преобразования представлений.
Браузерные службы и ожидание имеют собственную стоимость. В основном сравнении WebCrypto, IndexedDB, таймер и локальный запрос показали близкую скорость. Переход компилятора влияет на окружающий Dart-код; время браузерной службы продолжает участвовать в результате.
Общее ускорение подчиняется простой зависимости: чем больше исходного времени занимает ускоренная часть, тем заметнее выигрыш всего действия. Это выражается законом Амдала:
Общее ускорение = 1 / ((1 − p) + p / s)
p — доля исходного времени в ускоренной части
s — ускорение этой части
Веб-сборки Flutter уже сочетают технологии: JavaScript-вариант использует Wasm внутри CanvasKit, а Wasm-вариант запускает Dart через WasmGC и рисует через Skwasm. Поэтому эффект перехода складывается из компиляции Dart, рендерера, взаимодействия с браузером и характера нагрузки.
Подробности измерений и проверкиПодготовка и порядок запусковПеред реализацией сохранены гипотезы и протокол исследования. JavaScript-пилот подобрал количество повторов в пакете, после чего оно зафиксировано одинаковым для трёх режимов. Целевое время пакета составляло 25 мс, верхний предел — 512 повторов. После прогрева некоторые быстрые пакеты занимают меньше целевого времени; результаты нормированы на фактическое число повторов.
В основной серии выполнены все шесть перестановок порядка режимов. Для сопоставляемых сессий порядок задач перемешан одинаково. Во время замера таблица результатов остаётся неподвижной; данные прошлых запусков временно убираются из интерфейса и возвращаются после завершения. Между задачами предусмотрены паузы. Компиляция завершена до начала измерений.
Обычный вычислительный тест предварительно выполнялся минимум три пакета и 150 мс, максимум 200 пакетов. Для xorshift после диагностики установлен минимум 1 000 мс. UI-сценарий проходил один полный предварительный пакет. Дополнительная серия семи задач добавила 15 предварительных пакетов перед 21 учитываемым замером. Её план записан до запуска, все 36 сессий сохранены целиком.
Статистика и правильность ответовДоверительные интервалы получены bootstrap-пересчётом: 4 000 повторных выборок из парных сессионных отношений. Границы практической разницы — 1 / 1,10 и 1,10. Интервалы рассчитаны отдельно для каждой задачи; исследование имеет разведочный характер, с оценками без поправки на множественные сравнения. Кадры внутри одной сессии используются для описания распределения, а единицей статистического сравнения остаётся сессия.
Каждая задача возвращает используемый контрольный ответ. Между повторениями проверяются конечность значения и воспроизводимость. Целочисленные ответы должны точно совпадать между режимами; для скалярного произведения, математических функций, матриц и аудиофильтра принят относительный допуск 10⁻⁸. Основные целочисленные задачи сохраняют промежуточные значения в точном диапазоне JavaScript.
Дополнительно проверены известные значения xorshift, скалярного произведения, матриц, фильтра изображения, Fibonacci, Unicode, JSON, регулярных выражений и BigInt. Матрицы и фильтр сверены с отдельными реализациями на нескольких размерах. Для SHA-256 использован известный ответ для abc. Wasm-модули проверены на нескольких входах, а 64-битная арифметика — независимой реализацией Python.
В принятых в расчёт основных и дополнительных сессиях подтверждены фактическая компиляция Wasm, видимость страницы и успешное завершение задач. В основном опыте отдельно проверены изоляция страницы и рабочий поток отрисовки. Ошибки браузерной консоли отсутствовали. Автоматизация проверила выбор категории, ручной запуск, экспорт и повторное чтение результатов, переход между сборками, открытие отчёта и компоновку на узкой ширине.
Dart: независимые контрольные и граничные тестыimport 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:wasm_versus_javascript/kernels.dart';
void main(){
test('Independent known answers and boundary cases',(){expect(validateKernels()['passed'],15);});
test('Matrix product matches independent dot-product reference',(){
for(final n in [1,2,3,5,9]){
final a=List.generate(n*n,(i)=>(i%7-3).toDouble());
final b=List.generate(n*n,(i)=>(i%11-5).toDouble());
final got=multiply(a,b,n);
for(var row=0;row<n;row++){for(var col=0;col<n;col++){
final expected=List.generate(n,(k)=>a[row*n+k]*b[k*n+col]).fold<double>(0,(x,y)=>x+y);
expect(got[row*n+col],expected);
}}
}
});
test('Stencil matches an independent neighborhood implementation',(){
for(final w in [2,3,4,7,16]){
final values=Uint32List.fromList(List.generate(w*w,(i)=>(i*17)%256));
var expected=0;
for(var y=1;y<w-1;y++){for(var x=1;x<w-1;x++){
var pixel=0;for(final dy in [-1,0,1]){for(final dx in [-1,0,1]){pixel+=values[(y+dy)*w+x+dx];}}
expected+=pixel~/9;
}}expect(stencil(values,w),expected);
}
});
test('Seeded kernels return stable finite observable outputs',(){
final kernels=makeKernels(DataSet(20261009));
expect(kernels.map((k)=>k.id).toSet().length,kernels.length);
for(final k in kernels){final a=k.run(),b=k.run();expect(a.isFinite,true,reason:k.id);expect(a,b,reason:k.id);}
});
}
В первоначальном xorshift-тесте первые замеры JavaScript были быстрее последующих. Для проверки выполнены 18 дополнительных сессий: 15 предварительных пакетов и семь учитываемых. Отношение последних и первых трёх значений во всех этих сессиях попало в диапазон от 0,8 до 1,25. После этого прогрев xorshift увеличен, исправлены границы доступности элементов управления и стабилизирована UI-нагрузка. Все 18 основных сессий запущены заново.
Диагностическая серия дала JavaScript 1,680 мс, Wasm ST 0,619 мс, Wasm MT 0,619 мс. Она содержит 126 учитываемых и 270 предварительных пакетов. Окончательные основные таблицы используют полный повторный прогон.
Первый вариант 64-битного контроля позволял получить младший результат 32-битными операциями. После аудита добавлено перемешивание старших разрядов через сдвиг на каждом шаге. Обе исправленные задачи измерены в отдельной серии из 18 контекстов и семи пакетов. Новые 252 пакета заменили прежние в итоговых строках. Остальные 44 задачи сохранили основную серию. В окончательной основной таблице 5 796 пакетов.
В расширенной серии один запуск дал другую высоту абзацев. Это означало изменение результата работы, поэтому соответствующий блок сессии 10 целиком сохранён отдельно и повторён во всех трёх режимах. Архив включает 441 учитываемый и 315 предварительных пакетов этого блока. В итог вошёл повтор с одинаковой раскладкой текста во всех сборках.
Для этой статьи дополнительно завершена расширенная проверка семи задач, результаты которой показаны в начале и внутри соответствующих кейсов. Она хранится отдельной серией с тем же браузером и V8. Такое представление сохраняет и исходный результат, и поведение при увеличенном числе наблюдений.
Задачи с изменением скорости внутри основной сессииДиагностика сравнивает медиану последних и первых трёх замеров. Хотя бы одна сессия следующих задач вышла за диапазон отношения от 0,8 до 1,25: Обмен данными с фоновым обработчиком; Вычисления с условиями; Изменение обычной HTML-страницы. Все эти значения сохранены в расчётах. Возможные составляющие разброса — прогрев, сборка мусора и фоновая нагрузка.
Область применимостиВыводы относятся к описанным реализациям, размерам данных и указанному настольному Chrome на Apple M4. Повторения в контекстах одного процесса характеризуют одну среду. Температура, фоновые службы ОС, сборка мусора и кэши могут влиять на разброс. Входы микротестов повторяются, поэтому оптимизатор может специализировать код под них.
Подготовка данных выполняется до таймера, кроме тестов создания, копирования и кодирования. Затраты вызова и таймера входят в результаты. Работа с данными измеряется на конкретных объёмах — например, 16 КиБ и 4 МиБ для последовательного чтения. Такие рабочие наборы позволяют сравнить эти сценарии.
Сводная таблица основной серииВ этой таблице сохранены все исходные сопоставления. Ссылки ведут к разбору задачи, её коду и результатам расширенной серии, когда она проводилась.
Задача | JavaScript, мс | Wasm ST, мс | Wasm MT, мс | JS / ST · 95% | JS / MT · 95% | Результат основной серии |
|---|---|---|---|---|---|---|
1,678 | 0,617 | 0,615 | 2,72× [2,71; 2,72] | 2,72× [2,70; 2,72] | Wasm быстрее в 2,72 раза. | |
0,078 | 0,241 | 0,239 | 0,32× [0,32; 0,34] | 0,33× [0,32; 0,33] | JavaScript быстрее в 3,09 раза. | |
0,252 | 0,255 | 0,254 | 1,00× [0,99; 1,01] | 1,00× [0,99; 1,01] | Скорость практически одинакова. | |
0,307 | 0,443 | 0,443 | 0,69× [0,67; 0,72] | 0,69× [0,67; 0,72] | JavaScript быстрее в 1,44 раза. | |
0,349 | 0,184 | 0,182 | 1,88× [1,86; 1,97] | 1,91× [1,88; 1,97] | Wasm быстрее в 1,88 раза. | |
0,163 | 0,169 | 0,168 | 0,96× [0,95; 1,03] | 0,97× [0,94; 1,04] | Скорость практически одинакова. | |
0,631 | 0,809 | 0,654 | 0,77× [0,69; 0,97] | 0,99× [0,71; 1,08] | Скорость заметно меняется между запусками. | |
0,505 | 0,130 | 0,128 | 3,89× [3,84; 3,90] | 3,94× [3,89; 4,01] | Wasm быстрее в 3,89 раза. | |
1,319 | 0,475 | 0,475 | 2,78× [2,75; 2,84] | 2,80× [2,69; 2,84] | Wasm быстрее в 2,78 раза. | |
0,100 | 0,108 | 0,108 | 0,92× [0,91; 0,95] | 0,93× [0,91; 0,95] | Скорость практически одинакова. | |
0,349 | 0,190 | 0,187 | 1,85× [1,74; 1,89] | 1,87× [1,75; 1,92] | Wasm быстрее в 1,85 раза. | |
0,003 | 0,002 | 0,002 | 1,23× [1,00; 1,25] | 1,25× [1,00; 1,27] | Скорость заметно меняется между запусками. | |
0,642 | 0,531 | 0,529 | 1,20× [1,20; 1,22] | 1,22× [1,21; 1,22] | Wasm быстрее в 1,20 раза. | |
1,388 | 1,471 | 1,487 | 0,90× [0,78; 0,97] | 0,89× [0,77; 0,97] | Скорость заметно меняется между запусками. | |
0,059 | 0,070 | 0,070 | 0,83× [0,83; 0,84] | 0,84× [0,82; 0,85] | JavaScript быстрее в 1,20 раза. | |
0,101 | 0,169 | 0,169 | 0,59× [0,59; 0,60] | 0,60× [0,58; 0,60] | JavaScript быстрее в 1,68 раза. | |
0,078 | 0,084 | 0,084 | 0,92× [0,91; 0,94] | 0,93× [0,89; 0,95] | Разница находится около принятого порога 10%. | |
0,553 | 0,486 | 0,484 | 1,13× [1,11; 1,14] | 1,13× [1,12; 1,14] | Wasm быстрее в 1,13 раза. | |
3,441 | 3,679 | 3,738 | 0,94× [0,88; 1,40] | 0,92× [0,88; 1,41] | Скорость заметно меняется между запусками. | |
0,258 | 0,395 | 0,421 | 0,65× [0,61; 0,68] | 0,62× [0,61; 0,65] | JavaScript быстрее в 1,54 раза. | |
0,039 | 0,089 | 0,090 | 0,43× [0,43; 0,44] | 0,43× [0,43; 0,44] | JavaScript быстрее в 2,30 раза. | |
0,661 | 0,216 | 0,215 | 3,08× [1,86; 4,37] | 3,10× [1,91; 4,32] | Wasm быстрее в 3,08 раза. | |
0,055 | 0,174 | 0,174 | 0,32× [0,32; 0,33] | 0,32× [0,32; 0,33] | JavaScript быстрее в 3,14 раза. | |
0,153 | 0,284 | 0,286 | 0,54× [0,53; 0,55] | 0,54× [0,53; 0,55] | JavaScript быстрее в 1,85 раза. | |
0,470 | 0,340 | 0,341 | 1,38× [1,36; 1,40] | 1,38× [1,37; 1,38] | Wasm быстрее в 1,38 раза. | |
0,211 | 0,321 | 0,325 | 0,65× [0,65; 0,66] | 0,65× [0,65; 0,66] | JavaScript быстрее в 1,53 раза. | |
0,006 | 0,197 | 0,197 | 0,03× [0,03; 0,03] | 0,03× [0,03; 0,03] | JavaScript быстрее в 33,52 раза. | |
0,079 | 0,155 | 0,155 | 0,51× [0,50; 0,52] | 0,51× [0,50; 0,51] | JavaScript быстрее в 1,98 раза. | |
0,078 | 0,155 | 0,154 | 0,50× [0,50; 0,51] | 0,51× [0,51; 0,51] | JavaScript быстрее в 1,98 раза. | |
0,103 | 0,057 | 0,056 | 1,80× [1,77; 1,85] | 1,84× [1,80; 1,87] | Wasm быстрее в 1,80 раза. | |
0,024 | 0,024 | 0,024 | 1,02× [1,01; 1,03] | 1,02× [1,02; 1,03] | Скорость практически одинакова. | |
16,766 | 17,044 | 16,672 | 0,98× [0,92; 1,03] | 1,01× [0,98; 1,04] | Скорость практически одинакова. | |
0,565 | 0,523 | 0,498 | 1,07× [1,00; 1,10] | 1,13× [1,10; 1,16] | Скорость практически одинакова. | |
0,264 | 0,253 | 0,244 | 1,05× [1,02; 1,07] | 1,08× [1,06; 1,09] | Скорость практически одинакова. | |
0,046 | 0,045 | 0,045 | 0,99× [0,95; 1,16] | 1,00× [0,94; 1,12] | Разница находится около принятого порога 10%. | |
0,576 | 0,553 | 0,549 | 1,04× [1,00; 1,06] | 1,05× [1,04; 1,06] | Скорость практически одинакова. | |
3,473 | 3,226 | 3,194 | 1,08× [1,05; 1,13] | 1,09× [1,04; 1,15] | Разница находится около принятого порога 10%. | |
427,875 | 410,513 | 410,495 | 1,04× [1,04; 1,05] | 1,04× [1,04; 1,05] | Скорость практически одинакова. | |
410,195 | 409,913 | 410,355 | 1,00× [1,00; 1,00] | 1,00× [1,00; 1,00] | Скорость практически одинакова. | |
410,127 | 410,448 | 410,475 | 1,00× [1,00; 1,00] | 1,00× [1,00; 1,00] | Скорость практически одинакова. | |
410,485 | 410,653 | 410,580 | 1,00× [1,00; 1,00] | 1,00× [1,00; 1,00] | Скорость практически одинакова. | |
0,261 | 0,260 | 0,259 | 1,00× [0,99; 1,01] | 1,00× [0,99; 1,02] | Отдельный модуль: сравнение внутри кейса | |
0,070 | 0,069 | 0,069 | 1,01× [0,99; 1,03] | 1,01× [0,99; 1,02] | Отдельный модуль: сравнение внутри кейса | |
0,031 | 0,032 | 0,031 | 0,99× [0,94; 1,02] | 0,99× [0,98; 1,01] | Отдельный модуль: сравнение внутри кейса | |
0,181 | 0,179 | 0,180 | 1,01× [0,99; 1,02] | 1,01× [0,99; 1,02] | Отдельный модуль: сравнение внутри кейса | |
0,014 | 0,014 | 0,014 | 0,99× [0,98; 1,00] | 0,99× [0,97; 1,00] | Отдельный модуль: сравнение внутри кейса |
В приложении используются четыре Dart-файла: вычислительные задачи, измерительный цикл, экран Flutter и объявления взаимодействия с JavaScript. Ниже — их полный код вместе с браузерными обработчиками и конфигурацией.
Входные данные и вычислительные задачи — kernels.dartimport 'dart:convert';
import 'dart:math' as math;
import 'dart:typed_data';
/// Values keep all intermediate integer products below 2^53 on dart2js.
class DataSet {
DataSet(this.seed) {
var state = seed;
values = Uint32List(1 << 20);
for (var i = 0; i < values.length; i++) {
state = (state * 1664525 + 1013904223) & 0xffffffff;
values[i] = state & 65535;
}
text = List.generate(
2000,
(i) => 'item_$i: alpha βета 日本語 😀 ${values[i]}\n',
).join();
records = List.generate(
2000,
(i) => <String, Object>{
'id': i,
'name': 'item_$i',
'value': values[i],
'active': i.isEven,
},
);
json = jsonEncode(records);
}
final int seed;
late final Uint32List values;
late final String text;
late final List<Map<String, Object>> records;
late final String json;
}
class Kernel {
const Kernel(this.id, this.group, this.name, this.size, this.run);
final String id, group, name, size;
final num Function() run;
}
int bitMix(int seed, int count) {
var x = seed;
for (var i = 0; i < count; i++) {
x = (x ^ (x << 13)) & 0xffffffff;
x = (x ^ (x >> 17)) & 0xffffffff;
x = (x ^ (x << 5)) & 0xffffffff;
}
return x;
}
double dot(List<double> a, List<double> b) {
var sum = 0.0;
for (var i = 0; i < a.length; i++) {
sum += a[i] * b[i];
}
return sum;
}
Float64List multiply(List<double> a, List<double> b, int n) {
final out = Float64List(n * n);
for (var i = 0; i < n; i++) {
for (var k = 0; k < n; k++) {
final v = a[i * n + k];
for (var j = 0; j < n; j++) {
out[i * n + j] += v * b[k * n + j];
}
}
}
return out;
}
int stencil(Uint32List image, int width) {
var sum = 0;
for (var y = 1; y < width - 1; y++) {
for (var x = 1; x < width - 1; x++) {
final p = y * width + x;
sum +=
(image[p - width - 1] +
image[p - width] +
image[p - width + 1] +
image[p - 1] +
image[p] +
image[p + 1] +
image[p + width - 1] +
image[p + width] +
image[p + width + 1]) ~/
9;
}
}
return sum;
}
int fibonacci(int n) => n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
class Cell {
Cell(this.value, [this.next]);
final int value;
final Cell? next;
}
abstract class Operation {
int apply(int x);
}
class Add implements Operation {
@override
int apply(int x) => (x + 17) & 65535;
}
class Xor implements Operation {
@override
int apply(int x) => x ^ 8191;
}
class Multiply implements Operation {
@override
int apply(int x) => (x * 3) & 65535;
}
List<Kernel> makeKernels(DataSet data) {
final a = Float64List.fromList(
List.generate(65536, (i) => data.values[i] / 65536),
);
final b = Float64List.fromList(
List.generate(65536, (i) => data.values[i + 100] / 65536),
);
final matrixA = Float64List.fromList(a.take(64 * 64).toList());
final matrixB = Float64List.fromList(b.take(64 * 64).toList());
final copy = Uint32List(data.values.length);
final links = Uint32List(1 << 18);
// Odd multiplier and increment form a full-period permutation modulo 2^18.
for (var i = 0; i < links.length; i++) {
links[i] = (i * 1664525 + 1013904223) & (links.length - 1);
}
final map = {for (var i = 0; i < 20000; i++) 'key_$i': data.values[i]};
final keys = List.generate(20000, (i) => 'key_${(i * 7919) % 20000}');
final operations = <Operation>[Add(), Xor(), Multiply()];
final regular = RegExp(r'item_(\d+):');
final encoded = utf8.encode(data.text);
final big = (BigInt.one << 127) - BigInt.one;
final lanes=Int32x4List(65536);
final scalarLanes=Int32List(262144);
for(var i=0;i<scalarLanes.length;i++) {scalarLanes[i]=data.values[i]&4095;}
for(var i=0;i<lanes.length;i++) {lanes[i]=Int32x4(scalarLanes[i*4],scalarLanes[i*4+1],scalarLanes[i*4+2],scalarLanes[i*4+3]);}
return [
Kernel('dart_vector','Вычисления','Dart Int32x4: сумма векторов','65 536 векторов; API не гарантирует SIMD',(){
var sum=Int32x4(0,0,0,0);for(var i=0;i<lanes.length;i++) {sum=sum+lanes[i];}return sum.x+sum.y+sum.z+sum.w;
}),
Kernel('dart_scalar_vector','Вычисления','Dart int: контроль суммы векторов','262 144 целых; тот же результат',(){
var sum=0;for(var i=0;i<scalarLanes.length;i++) {sum+=scalarLanes[i];}return sum;
}),
Kernel(
'int32',
'Вычисления',
'Целые: xorshift / битовые операции',
'300 000 шагов; u32',
() => bitMix(data.seed, 300000),
),
Kernel(
'double_dot',
'Вычисления',
'Float64: скалярное произведение',
'65 536 пар',
() => dot(a, b),
),
Kernel(
'transcendental',
'Вычисления',
'sin / cos / sqrt',
'30 000 аргументов',
() {
var sum = 0.0;
for (var i = 0; i < 30000; i++) {
final x = a[i];
sum += math.sin(x) * math.cos(x) + math.sqrt(x);
}
return sum;
},
),
Kernel(
'matrix',
'Вычисления',
'Матрицы: обычный тройной цикл',
'64×64; Float64; включая output',
() => multiply(matrixA, matrixB, 64).fold(0.0, (x, y) => x + y),
),
Kernel(
'image_stencil',
'Вычисления',
'Изображение: box-filter 3×3',
'256×256; без декодера/GPU',
() => stencil(data.values, 256),
),
Kernel(
'audio_fir',
'Вычисления',
'Аудио: FIR-фильтр',
'8 192 отсчёта × 32 taps',
() {
var sum = 0.0;
for (var i = 32; i < 8192; i++) {
for (var j = 0; j < 32; j++) {
sum += a[i - j] * b[j];
}
}
return sum;
},
),
Kernel(
'branch',
'Вычисления',
'Условные переходы по данным',
'262 144 значений',
() {
var sum = 0;
for (var i = 0; i < 262144; i++) {
final v = data.values[i];
if (v < 20000) {
sum += v * 3;
} else if (v < 40000) {
sum -= v;
} else {
sum += v ~/ 3;
}
}
return sum;
},
),
Kernel(
'recursion',
'Вычисления',
'Рекурсивные вызовы: Fibonacci',
'n=25',
() => fibonacci(25),
),
for (final length in [4096, 1048576])
Kernel(
'memory_$length',
'Память',
'Последовательное чтение ${length == 4096 ? '16 KiB' : '4 MiB'}',
'$length × Uint32',
() {
var sum = 0;
for (var i = 0; i < length; i++) {
sum += data.values[i];
}
return sum;
},
),
Kernel(
'random_memory',
'Память',
'Зависимое случайное чтение',
'1 MiB; 262 144 переходов',
() {
var p = data.seed & (links.length - 1), sum = 0;
for (var i = 0; i < links.length; i++) {
p = links[p];
sum += p;
}
return sum;
},
),
Kernel(
'bulk_copy',
'Память',
'TypedData.setAll: копирование',
'4 MiB; заранее выделенный output',
() {
copy.setAll(0, data.values);
return copy[0] + copy[copy.length - 1];
},
),
Kernel(
'allocation_short',
'Объекты',
'Короткоживущие объекты',
'32 партии × 1 024 Cell',
() {
var sum = 0;
for (var round = 0; round < 32; round++) {
final cells = List.generate(
1024,
(i) => Cell(data.values[i + round]),
);
for (final c in cells) {
sum += c.value;
}
}
return sum;
},
),
Kernel(
'allocation_graph',
'Объекты',
'Объектный граф и проход по ссылкам',
'32 768 связанных Cell; allocation включён',
() {
Cell? head;
for (var i = 0; i < 32768; i++) {
head = Cell(data.values[i], head);
}
var sum = 0;
while (head != null) {
sum += head.value;
head = head.next;
}
return sum;
},
),
Kernel(
'dispatch',
'Объекты',
'Полиморфные вызовы',
'300 000 вызовов; 3 реализации',
() {
var x = data.seed & 65535;
for (var i = 0; i < 300000; i++) {
x = operations[i % 3].apply(x);
}
return x;
},
),
Kernel(
'sort',
'Коллекции',
'Сортировка чисел',
'20 000 значений; копирование включено',
() {
final out = data.values.take(20000).toList()..sort();
var sum = 0;
for (var i = 0; i < out.length; i++) {
sum = (sum + out[i] * (i + 1)) % 1000000007;
}
return sum;
},
),
Kernel(
'map',
'Коллекции',
'Хеш-таблица: поиск строковых ключей',
'20 000 поисков; Map подготовлен',
() {
var sum = 0;
for (final k in keys) {
sum += map[k]!;
}
return sum;
},
),
Kernel(
'string_build',
'Текст',
'StringBuffer / форматирование',
'2 000 строк',
() {
final out = StringBuffer();
for (var i = 0; i < 2000; i++) {
out.write('item_$i:${data.values[i]};');
}
return out.toString().length;
},
),
Kernel(
'utf8_encode',
'Текст',
'UTF-8: кодирование Unicode',
'2 000 многоязычных строк',
() => utf8.encode(data.text).fold<int>(0, (a, b) => a + b),
),
Kernel(
'utf8_decode',
'Текст',
'UTF-8: декодирование Unicode',
'2 000 многоязычных строк',
() => utf8.decode(encoded).length,
),
Kernel('regexp', 'Текст', 'RegExp: поиск и группы', '2 000 совпадений', () {
var sum = 0;
for (final m in regular.allMatches(data.text)) {
sum += int.parse(m.group(1)!);
}
return sum;
}),
Kernel(
'json_encode',
'Сериализация',
'JSON: encode',
'2 000 объектов × 4 поля',
() => jsonEncode(data.records).length,
),
Kernel(
'json_decode',
'Сериализация',
'JSON: decode и чтение поля',
'2 000 объектов × 4 поля',
() {
final rows = jsonDecode(data.json) as List;
var sum = 0;
for (final r in rows) {
sum += (r['value'] as num).toInt();
}
return sum;
},
),
Kernel(
'bigint',
'Вычисления',
'BigInt: точная арифметика',
'1 000 modular multiply; 127 бит',
() {
var x = BigInt.from(data.seed);
for (var i = 0; i < 1000; i++) {
x = (x * x + BigInt.from(i)) % big;
}
return (x & BigInt.from(0x7fffffff)).toInt();
},
),
];
}
/// Independent reference cases; executed in BOTH release browser targets too.
Map<String, Object> validateKernels() {
void check(bool yes, String name) {
if (!yes) throw StateError('Reference failed: $name');
}
check(bitMix(1, 0) == 1, 'xorshift empty');
check(bitMix(1, 1) == 270369, 'xorshift known vector');
check(bitMix(1, 2) == 67634689, 'xorshift second vector');
check(dot([], []) == 0, 'dot empty');
check(dot([1, 2, 3], [4, 5, 6]) == 32, 'dot exact');
check((dot([0.1, 0.2], [0.3, 0.4]) - 0.11).abs() < 1e-12, 'dot fractional');
check(
multiply([1, 2, 3, 4], [5, 6, 7, 8], 2).asMap().entries.every((e)=>e.value==[19,22,43,50][e.key]),
'matrix known answer',
);
check(multiply([2.5], [4], 1).single == 10, 'matrix scalar');
check(
stencil(Uint32List.fromList(List.filled(9, 9)), 3) == 9,
'filter constant',
);
check(
stencil(Uint32List.fromList(List.generate(9, (i) => i)), 3) == 4,
'filter ramp',
);
check(
fibonacci(0) == 0 && fibonacci(1) == 1 && fibonacci(10) == 55,
'fibonacci references',
);
check(
utf8.decode(utf8.encode('β 日本語 😀')) == 'β 日本語 😀',
'unicode roundtrip',
);
check(jsonDecode('{"n":17,"b":true}')["n"] == 17, 'JSON known answer');
check(
RegExp(r'item_(\d+)').firstMatch('item_123')!.group(1) == '123',
'regexp reference',
);
check(
BigInt.from(7).modPow(BigInt.from(3), BigInt.from(13)) == BigInt.from(5),
'BigInt reference',
);
return {'passed': 15, 'status': 'passed'};
}
import 'dart:convert';
import 'dart:js_interop';
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/scheduler.dart';
import 'bridge.dart' as bridge;
import 'kernels.dart';
const protocolVersion='1.1';
const dataSeed=20261009;
const defaultSamples=7;
const targetBatchMs=25.0;
const warmupMs=150.0;
const integerWarmupMs=1000.0;
const maxRepeats=512;
const uiFrames=24;
final frameTimings=<Map<String,num>>[];
void recordFrameTimings(List<ui.FrameTiming> frames) {
for(final f in frames) {
frameTimings.add({'startMs':f.timestampInMicroseconds(ui.FramePhase.buildStart)/1000,
'buildMs':f.buildDuration.inMicroseconds/1000,'rasterMs':f.rasterDuration.inMicroseconds/1000,
'totalMs':f.totalSpan.inMicroseconds/1000});
}
}
double median(List<double> values) {
final sorted=[...values]..sort(); final n=sorted.length;
return n.isOdd?sorted[n~/2]:(sorted[n~/2-1]+sorted[n~/2])/2;
}
class Workload {
Workload(this.id,this.group,this.name,this.size,{this.sync,this.async});
final String id,group,name,size;
final num Function()? sync;
final Future<num> Function()? async;
Map<String,Object> get description=>{'id':id,'group':group,'name':name,'size':size};
Future<num> batch(int repeats) async {
num checksum=0;
if(sync!=null) {for(var i=0;i<repeats;i++) {checksum+=sync!();}}
else {for(var i=0;i<repeats;i++) {checksum+=await async!();}}
return checksum/repeats;
}
}
List<Workload> workloads(Future<num> Function(String) uiRun) {
final data=DataSet(dataSeed);
final interop=Uint32List.fromList(data.values.take(65536).toList());
return [
for(final k in makeKernels(data)) Workload(k.id,k.group,k.name,k.size,sync:k.run),
Workload('interop_scalar','Граница JS','Dart → JS: маленькие вызовы','20 000 вызовов',sync:(){
var sum=0.0; for(var i=0;i<20000;i++) {sum+=bridge.scalar(i.toDouble());} return sum;
}),
Workload('interop_batch','Граница JS','Dart → JS: один пакет','65 536 чисел; toJS + sum',sync:()=>bridge.sum(interop.toJS)),
Workload('interop_roundtrip','Граница JS','TypedData: toJS → toDart','65 536 чисел; включая доступ JS',sync:(){
final js=interop.toJS; final sum=bridge.sum(js); return sum+js.toDart.last;
}),
Workload('microtasks','Асинхронность','Future / microtasks','1 000 await Future.value',async:()async{
var sum=0;for(var i=0;i<1000;i++) {sum+=await Future.value(i);}return sum;
}),
for(final c in const [
['webcrypto','WebCrypto: SHA-256','64 KiB; браузерный native API'],
['timer_wait','Ожидание таймера','15 ms requested; elapsed measured'],
['local_fetch','Локальный HTTP fetch','64 KiB; no-store; без WAN'],
['indexeddb','IndexedDB: put/get/delete','64 KiB; одна транзакция'],
['worker_transfer','Web Worker: transfer + ответ','64 KiB; копирование и суммирование'],
]) Workload(c[0],'Браузер / I/O',c[1],c[2],async:()async=>(await bridge.nativeAsync(c[0].toJS).toDart).toDartDouble),
Workload('dom_layout','Браузер / I/O','DOM: запись стиля + forced layout','100 layout flushes; общий JS helper',sync:()=>bridge.nativeSync('dom_layout'.toJS)),
for(final c in const [
['external_js_sum','Сумма: написанный вручную JS','262 144 uint32; 1 MiB'],
['external_wasm_sum','Сумма: скалярный Wasm','тот же массив, точный i32 result'],
['external_simd_sum','Сумма: явный Wasm SIMD','i32x4.add; тот же массив'],
['external_js_i64','Точные 64 бита: JS BigInt','10 000 multiply/xor modulo 2^64'],
['external_wasm_i64','Точные 64 бита: Wasm i64','тот же алгоритм и точный результат'],
]) Workload(c[0],'Контрольные Wasm-модули',c[1],c[2],sync:()=>bridge.nativeSync(c[0].toJS)),
Workload('text_layout','Flutter', 'Paragraph: формирование и layout','100 уникальных абзацев',sync:(){
var height=0.0;
for(var i=0;i<100;i++) {
final builder=ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily:'Roboto',fontSize:14))
..addText('Paragraph $i: Flutter text layout and measurement. ' * 3);
final p=builder.build()..layout(const ui.ParagraphConstraints(width:300));height+=p.height;p.dispose();
}return height;
}),
for(final c in const [
['ui_widgets','Widget build + layout','600 ячеек; 24 обновления'],
['ui_scroll','Прокрутка списка','50 000 строк; 24 jumpTo'],
['ui_paint','Canvas: векторная отрисовка','2 000 кругов × 24 кадра'],
['ui_layers','Canvas: clipping / saveLayer','100 слоёв × 24 кадра'],
]) Workload(c[0],'Flutter',c[1],c[2],async:()=>uiRun(c[0])),
];
}
Future<Map<String,Object?>> measure({
required List<Workload> cases,
required Map<String,dynamic> config,
required void Function(String, Map<String,Object?>?) progress,
}) async {
final metadata=jsonDecode(bridge.metadata().toDart) as Map<String,dynamic>;
const wasm=bool.fromEnvironment('dart.tool.dart2wasm');
final mode=wasm?(metadata['single']==true?'wasm-st':'wasm-mt'):'js';
final validation=validateKernels();
final count=(config['samples'] as num? ?? defaultSamples).toInt().clamp(1,31);
final session=(config['session'] as num? ?? 0).toInt();
final pilot=config['pilot']==true;
final supplied=Map<String,dynamic>.from(config['repeats'] as Map? ?? {});
final only=(config['only'] as List?)?.cast<String>();
final selected=cases.where((c)=>only==null||only.contains(c.id)).toList();
selected.shuffle(math.Random(dataSeed+session));
final results=<Map<String,Object?>>[];
final chosen=<String,int>{};
final began=DateTime.now().toUtc().toIso8601String();
bridge.startRun(); frameTimings.clear();
for(final c in selected) {
progress(c.name,null); bridge.status(c.id.toJS);
await Future<void>.delayed(const Duration(milliseconds:30));
try {
var repeats=(supplied[c.id] as num?)?.toInt() ?? 1;
final isUi=c.id.startsWith('ui_');
if((pilot||!supplied.containsKey(c.id))&&!isUi) {
// Calibration occurs before measured samples, never across target variants.
await c.batch(1);
final t=bridge.clockNow(); await c.batch(1); final elapsed=bridge.clockNow()-t;
repeats=(targetBatchMs/math.max(elapsed,0.02)).ceil().clamp(1,maxRepeats);
}
if(isUi) repeats=1;
if(repeats<1||repeats>maxRepeats) throw ArgumentError('invalid batch size');
chosen[c.id]=repeats;
final warmStart=bridge.clockNow();var warmCount=0;
final warm=<double>[];
do {
final t=bridge.clockNow(); await c.batch(repeats);warm.add(bridge.clockNow()-t);warmCount++;
}while(!isUi && (warmCount<3||bridge.clockNow()-warmStart<(c.id=='int32'?integerWarmupMs:warmupMs)) && warmCount<200);
final samples=<double>[]; final checks=<num>[]; final ranges=<List<double>>[];
for(var sample=0;sample<count;sample++) {
final t=bridge.clockNow();final value=await c.batch(repeats);final elapsed=bridge.clockNow()-t;
if(!value.isFinite)throw StateError('Non-finite result');
if(checks.isNotEmpty&&(value-checks.first).abs()>1e-8*math.max(1,checks.first.abs()))throw StateError('Unstable output');
checks.add(value);samples.add(elapsed/repeats); if(isUi) ranges.add([t,t+elapsed]);
await Future<void>.delayed(const Duration(milliseconds:5));
}
final result=<String,Object?>{...c.description,'repeats':repeats,'samplesMs':samples,
'medianMs':median(samples),'minMs':samples.reduce(math.min),'maxMs':samples.reduce(math.max),
'result':checks.first,'warmupBatchMs':warm,'status':'passed','frameRangesMs':ranges,
'metric':isUi?'24 scheduled Flutter frames; wall ms':'wall ms per workload'};
results.add(result);progress(c.name,result);
}catch(e) {
final result=<String,Object?>{...c.description,'status':'failed','error':e.toString()};
results.add(result);progress(c.name,result);
}
}
await Future<void>.delayed(const Duration(milliseconds:140));
SchedulerBinding.instance.scheduleFrame();
await SchedulerBinding.instance.endOfFrame;
await Future<void>.delayed(const Duration(milliseconds:40));
for(final r in results) {
final ranges=r['frameRangesMs'] as List<List<double>>? ?? [];
if(ranges.isNotEmpty) r['frameTimings']=frameTimings.where((f)=>ranges.any((w)=>f['startMs']!>=w[0]&&f['startMs']!<=w[1])).toList();
}
final result=<String,Object?>{'protocol':protocolVersion,'mode':mode,'actualWasm':wasm,
'session':session,'pilot':pilot,'startedUtc':began,'finishedUtc':DateTime.now().toUtc().toIso8601String(),
'environment':metadata,'firstFrameMs':bridge.firstFrameMs,'validation':validation,
'visibleThroughout':bridge.validVisibility(),'repeats':chosen,'results':results,
'comparableBatches':supplied.isNotEmpty,'seed':dataSeed};
bridge.publish(jsonEncode(result).toJS);bridge.status('done'.toJS);
return result;
}
import 'dart:convert';
import 'dart:js_interop';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:web/web.dart' as web;
import 'benchmark.dart';
import 'bridge.dart' as bridge;
void main()=>runApp(const BenchmarkApp());
class BenchmarkApp extends StatelessWidget {
const BenchmarkApp({super.key});
@override Widget build(BuildContext context)=>MaterialApp(
title:'WASM versus JavaScript',debugShowCheckedModeBanner:false,
theme:ThemeData(colorScheme:ColorScheme.fromSeed(seedColor:const Color(0xff38bdf8),brightness:Brightness.dark),
scaffoldBackgroundColor:const Color(0xff0b1220),useMaterial3:true),
home:const BenchmarkScreen());
}
class BenchmarkScreen extends StatefulWidget {
const BenchmarkScreen({super.key});
@override State<BenchmarkScreen> createState()=>_BenchmarkScreenState();
}
class _BenchmarkScreenState extends State<BenchmarkScreen> {
late final List<Workload> cases;
bool running=false,ready=false;
String active='Подготовка',scene='';int tick=0;
Map<String,Object?>? current;
final rows=<String,Map<String,Object?>>{};
List<dynamic> saved=[];
Map<String,dynamic> reportSummary={};
String group='Все задачи';
final scroll=ScrollController();
@override void initState(){super.initState();cases=workloads(runScene);SchedulerBinding.instance.addTimingsCallback(recordFrameTimings);initialize();}
Future<void> initialize()async{
try{
await bridge.prepare().toDart;
saved=jsonDecode(bridge.saved().toDart) as List;
bridge.install(((JSString c)=>execute(jsonDecode(c.toDart) as Map<String,dynamic>).toJS).toJS);
setState((){ready=true;active='Готово к измерению';});
await SchedulerBinding.instance.endOfFrame;
bridge.markReady();
try {
final response=await web.window.fetch('../results/summary.json'.toJS).toDart;
if(response.ok){final s=await response.text().toDart;if(mounted)setState(()=>reportSummary=jsonDecode(s.toDart));}
}catch(_){}
}catch(e){setState(()=>active='Ошибка подготовки: $e');bridge.status(active.toJS);}
}
Future<JSString> execute(Map<String,dynamic> config)async{
if(running)throw StateError('Benchmark already running');
final previousSummary=reportSummary;var savedAfterRun=saved;
setState((){running=true;rows.clear();current=null;reportSummary={};saved=[];});
try {
final result=await measure(cases:cases,config:config,progress:(name,row){
if(mounted)setState((){active=name;if(row!=null)rows[row['id'] as String]=row;});
});
if(mounted)setState((){current=result;active='Завершено';scene='';});
if(config['pilot']!=true){bridge.save(jsonEncode(result).toJS);savedAfterRun=jsonDecode(bridge.saved().toDart) as List;}
return jsonEncode(result).toJS;
}catch(e){if(mounted)setState(()=>active='Ошибка: $e');bridge.status(active.toJS);rethrow;}finally{if(mounted)setState((){running=false;reportSummary=previousSummary;saved=savedAfterRun;});}
}
Future<void> manualRun()async{
Map<String,dynamic> repeats={};
try{
final r=await web.window.fetch('../results/calibration.json'.toJS).toDart;
if(r.ok)repeats=jsonDecode((await r.text().toDart).toDart);
}catch(_){}
await execute({'repeats':repeats,'samples':defaultSamples,
if(group!='Все задачи')'only':cases.where((c)=>c.group==group).map((c)=>c.id).toList()});
}
Future<num> runScene(String id)async{
setState((){scene=id;tick=0;});
await SchedulerBinding.instance.endOfFrame;
num checksum=0;
for(var i=0;i<uiFrames;i++){
setState(()=>tick=i+1);
if(id=='ui_scroll'&&scroll.hasClients){scroll.jumpTo((i*127.0)%scroll.position.maxScrollExtent);}
await SchedulerBinding.instance.endOfFrame;
checksum+=i+1;
}
return checksum;
}
String get mode {
const wasm=bool.fromEnvironment('dart.tool.dart2wasm');
if(!wasm)return 'JavaScript · CanvasKit';
final single=Uri.base.queryParameters.containsKey('single');
return 'WebAssembly · skwasm ${single?'ST':'MT'}';
}
@override void dispose(){SchedulerBinding.instance.removeTimingsCallback(recordFrameTimings);scroll.dispose();super.dispose();}
Widget sceneWidget(){
switch(scene){
case 'ui_widgets':return Wrap(children:List.generate(600,(i)=>SizedBox(width:34,height:20,
child:ColoredBox(color:Color(0xff17304b+((i+tick)%12)*512),child:Text('${(i+tick)%99}',style:const TextStyle(fontSize:10))))));
case 'ui_scroll':return ListView.builder(controller:scroll,itemCount:50000,itemExtent:42,
itemBuilder:(context,i)=>ListTile(dense:true,leading:const Icon(Icons.data_object),title:Text('Record $i'),trailing:Text('${i*17}')));
case 'ui_paint':case 'ui_layers':return CustomPaint(painter:StressPainter(tick,scene=='ui_layers'),size:Size.infinite);
default:return Center(child:Column(mainAxisSize:MainAxisSize.min,children:[
const Icon(Icons.science_outlined,size:48,color:Color(0xff38bdf8)),const SizedBox(height:12),
Text('${cases.length} задач · 3 режима',style:Theme.of(context).textTheme.titleLarge),
const SizedBox(height:8),const Text('Вычисления · память · браузер · Flutter'),
]));
}
}
Widget metric(String value,String label)=>Container(padding:const EdgeInsets.all(16),
decoration:BoxDecoration(color:const Color(0xff152239),borderRadius:BorderRadius.circular(12)),
child:Column(crossAxisAlignment:CrossAxisAlignment.start,children:[Text(value,style:const TextStyle(fontSize:25,fontWeight:FontWeight.w600)),Text(label)]));
String resultText(Workload c){
final row=rows[c.id];if(row!=null)return row['status']=='passed'?'${(row['medianMs'] as num).toStringAsFixed(3)} ms':'Ошибка';
return '—';
}
@override Widget build(BuildContext context){
final selected=cases.where((c)=>group=='Все задачи'||c.group==group).toList();
final narrow=MediaQuery.sizeOf(context).width<700;
final summaries=Map<String,dynamic>.from(reportSummary['tasks'] as Map? ?? {});
return Scaffold(appBar:AppBar(title:const Text('WASM versus JavaScript'),actions:[
IconButton(tooltip:'Отчёт исследования',onPressed:()=>web.window.open('../report.html','_blank'),icon:const Icon(Icons.article_outlined)),
IconButton(tooltip:'Экспорт измерений',onPressed:current==null?null:()=>bridge.download(jsonEncode(current).toJS),icon:const Icon(Icons.download)),
]),body:SafeArea(child:ListView(padding:EdgeInsets.all(narrow?14:28),children:[
Text('Лаборатория производительности',style:Theme.of(context).textTheme.headlineMedium),
const SizedBox(height:8),Text(mode,style:const TextStyle(color:Color(0xff7dd3fc),fontSize:17)),
const SizedBox(height:18),Wrap(spacing:12,runSpacing:12,children:[
metric('${cases.length}','задач'),metric('${rows.length}','выполнено'),metric('$defaultSamples','замеров на задачу'),
if(reportSummary['sessionsPerMode']!=null)metric('${reportSummary['sessionsPerMode']}','сессий на режим в отчёте'),
]),
const SizedBox(height:18),Wrap(spacing:12,runSpacing:10,crossAxisAlignment:WrapCrossAlignment.center,children:[
Semantics(container:true,explicitChildNodes:true,child:SizedBox(width:narrow?double.infinity:280,child:DropdownButtonFormField<String>(initialValue:group,
decoration:const InputDecoration(labelText:'Тип задач',border:OutlineInputBorder()),
items:['Все задачи',...cases.map((c)=>c.group).toSet()].map((g)=>DropdownMenuItem(value:g,child:Text(g))).toList(),
onChanged:running?null:(value)=>setState(()=>group=value!)))),
Semantics(identifier:'benchmark.run',child:FilledButton.icon(onPressed:ready&&!running?manualRun:null,
icon:const Icon(Icons.play_arrow),label:const Text('Запустить тесты'))),
OutlinedButton(onPressed:running?null:()=>web.window.location.href='../js/',child:const Text('JavaScript')),
OutlinedButton(onPressed:running?null:()=>web.window.location.href='../wasm/?single=1',child:const Text('Wasm ST')),
OutlinedButton(onPressed:running?null:()=>web.window.location.href='../wasm/',child:const Text('Wasm MT')),
]),
const SizedBox(height:12),Text(active),if(running)const Padding(padding:EdgeInsets.only(top:8),child:LinearProgressIndicator()),
const SizedBox(height:18),Container(height:360,clipBehavior:Clip.hardEdge,
decoration:BoxDecoration(color:const Color(0xff101d31),border:Border.all(color:const Color(0xff24364e)),borderRadius:BorderRadius.circular(12)),child:sceneWidget()),
const SizedBox(height:24),Text('Измерения и сравнение',style:Theme.of(context).textTheme.titleLarge),
const SizedBox(height:8),const Text('Время меньше — лучше. Ускорение = JS / Wasm. Значения отчёта получены в Chrome на компьютере.'),
const SizedBox(height:12),
for(final c in selected) Card(margin:const EdgeInsets.only(bottom:8),child:Padding(padding:const EdgeInsets.all(14),
child:Column(crossAxisAlignment:CrossAxisAlignment.start,children:[
Row(crossAxisAlignment:CrossAxisAlignment.start,children:[Expanded(child:Text(c.name,style:const TextStyle(fontWeight:FontWeight.w600))),Text(resultText(c),style:const TextStyle(color:Color(0xff7dd3fc)))]),
const SizedBox(height:5),Text('${c.group} · ${c.size}',style:const TextStyle(color:Color(0xffa4b5cb),fontSize:12)),
if(summaries[c.id]!=null)...[
const SizedBox(height:8),Text(summaries[c.id]['display'] as String),
]else if(saved.isNotEmpty)...[
const SizedBox(height:8),Wrap(spacing:16,children:[for(final run in saved)
if((run['results'] as List).any((r)=>r['id']==c.id&&r['status']=='passed'))
Text('${run['mode']}: ${((run['results'] as List).firstWhere((r)=>r['id']==c.id)['medianMs'] as num).toStringAsFixed(3)} ms')]),
],
]))),
])));
}
}
class StressPainter extends CustomPainter {
StressPainter(this.tick,this.layers);final int tick;final bool layers;
@override void paint(Canvas canvas,Size size){
final paint=Paint();
if(layers){
for(var i=0;i<100;i++){
final x=(i*31+tick*2)%size.width,y=(i*17)%size.height;
final r=Rect.fromLTWH(x,y,72,60);canvas.saveLayer(r,Paint()..color=const Color(0xaaeeeeee));
canvas.clipRRect(RRect.fromRectAndRadius(r,const Radius.circular(14)));
canvas.drawRect(r,paint..color=const Color(0xff38bdf8));
canvas.drawCircle(Offset(x+24,y+24),22,paint..color=const Color(0xffa78bfa));canvas.restore();
}
}else{
for(var i=0;i<2000;i++){
paint.color=Color.fromARGB(160,(i*17)%256,(i*31)%256,190);
canvas.drawCircle(Offset((i*37+tick*3)%size.width,(i*19)%size.height),3+(i%7).toDouble(),paint);
}
}
}
@override bool shouldRepaint(StressPainter old)=>old.tick!=tick||old.layers!=layers;
}
import 'dart:js_interop';
@JS('bench.now') external double clockNow();
@JS('bench.metadata') external JSString metadata();
@JS('bench.install') external void install(JSFunction callback);
@JS('bench.prepare') external JSPromise<JSNumber> prepare();
@JS('bench.publish') external void publish(JSString json);
@JS('bench.status') external void status(JSString text);
@JS('bench.markReady') external void markReady();
@JS('bench.start') external void startRun();
@JS('bench.visible') external bool validVisibility();
@JS('bench.save') external void save(JSString json);
@JS('bench.saved') external JSString saved();
@JS('bench.download') external void download(JSString json);
@JS('bench.firstFrameMs') external double get firstFrameMs;
@JS('bench.scalar') external double scalar(double x);
@JS('bench.sum') external double sum(JSUint32Array array);
@JS('bench.nativeSync') external double nativeSync(JSString id);
@JS('bench.nativeAsync') external JSPromise<JSNumber> nativeAsync(JSString id);
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:wasm_versus_javascript/kernels.dart';
void main(){
test('Independent known answers and boundary cases',(){expect(validateKernels()['passed'],15);});
test('Matrix product matches independent dot-product reference',(){
for(final n in [1,2,3,5,9]){
final a=List.generate(n*n,(i)=>(i%7-3).toDouble());
final b=List.generate(n*n,(i)=>(i%11-5).toDouble());
final got=multiply(a,b,n);
for(var row=0;row<n;row++){for(var col=0;col<n;col++){
final expected=List.generate(n,(k)=>a[row*n+k]*b[k*n+col]).fold<double>(0,(x,y)=>x+y);
expect(got[row*n+col],expected);
}}
}
});
test('Stencil matches an independent neighborhood implementation',(){
for(final w in [2,3,4,7,16]){
final values=Uint32List.fromList(List.generate(w*w,(i)=>(i*17)%256));
var expected=0;
for(var y=1;y<w-1;y++){for(var x=1;x<w-1;x++){
var pixel=0;for(final dy in [-1,0,1]){for(final dx in [-1,0,1]){pixel+=values[(y+dy)*w+x+dx];}}
expected+=pixel~/9;
}}expect(stencil(values,w),expected);
}
});
test('Seeded kernels return stable finite observable outputs',(){
final kernels=makeKernels(DataSet(20261009));
expect(kernels.map((k)=>k.id).toSet().length,kernels.length);
for(final k in kernels){final a=k.run(),b=k.run();expect(a.isFinite,true,reason:k.id);expect(a,b,reason:k.id);}
});
}
'use strict';
window.bench = {
version:'1.0', ready:false, result:null, progress:'loading', error:null, hiddenDuringRun:false,
scalar:x=>x+1, sum:a=>{let s=0;for(let i=0;i<a.length;i++)s+=a[i];return s;},
now:()=>performance.now(), publish:text=>{bench.result=JSON.parse(text);},
status:text=>{bench.progress=text;}, install:fn=>{bench.run=fn;},
metadata:()=>JSON.stringify({userAgent:navigator.userAgent,hardwareConcurrency:navigator.hardwareConcurrency,
deviceMemoryGiB:navigator.deviceMemory??null,isolated:crossOriginIsolated,secure:isSecureContext,
width:innerWidth,height:innerHeight,dpr:devicePixelRatio,visibility:document.visibilityState,
single:new URLSearchParams(location.search).has('single'),
rendererResources:performance.getEntriesByType('resource').map(x=>x.name).filter(x=>/skwasm|canvaskit/.test(x))}),
markReady:()=>{bench.firstFrameMs=performance.now();bench.ready=true;document.getElementById('loading')?.remove();},
start:()=>{bench.hiddenDuringRun=document.hidden;bench.result=null;},
visible:()=>!bench.hiddenDuringRun&&!document.hidden,
save:text=>{const r=JSON.parse(text);localStorage.setItem(`wasm-vs-js:${r.mode}`,text);},
saved:()=>JSON.stringify(Object.keys(localStorage).filter(k=>k.startsWith('wasm-vs-js:')).map(k=>JSON.parse(localStorage.getItem(k)))),
download:text=>{const url=URL.createObjectURL(new Blob([text],{type:'application/json'}));
const a=document.createElement('a');a.href=url;a.download=`wasm-versus-javascript-${Date.now()}.json`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);},
};
document.addEventListener('visibilitychange',()=>{if(document.hidden)bench.hiddenDuringRun=true;});
bench.prepare=async()=>{
const {instance}=await WebAssembly.instantiateStreaming(fetch('kernels.wasm'));bench.wasm=instance.exports;
bench.ints=new Uint32Array(bench.wasm.memory.buffer,0,262144);
for(let i=0;i<bench.ints.length;i++)bench.ints[i]=(i*17+11)&4095;
for(const n of [0,4,8,262144]){const expected=bench.sum(bench.ints.subarray(0,n));
if(bench.wasm.sum(n)!==expected||bench.wasm.simd(n)!==expected)throw Error('Wasm sum validation failed');}
bench.i64js=n=>{let x=1234567n;for(let i=0;i<n;i++){x=BigInt.asUintN(64,x*6364136223846793005n^BigInt(i));x^=x>>33n;}return Number(x&2147483647n);};
for(const [n,expected] of [[0, 1234567], [1, 1812802702], [2, 1046528860], [10, 591709396], [10000, 1507571436]])if(bench.i64js(n)!==expected||bench.wasm.i64mix(n)!==expected)throw Error('Wasm i64 validation failed');
bench.bytes=new Uint8Array(65536);for(let i=0;i<bench.bytes.length;i++)bench.bytes[i]=i&255;
const abc=await crypto.subtle.digest('SHA-256',new TextEncoder().encode('abc'));
if([...new Uint8Array(abc)].map(x=>x.toString(16).padStart(2,'0')).join('')!=='ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')throw Error('SHA reference failed');
bench.worker=new Worker('worker.js');
bench.db=await new Promise((resolve,reject)=>{const r=indexedDB.open('wasm-versus-javascript-benchmark-v1',1);
r.onupgradeneeded=()=>r.result.createObjectStore('scratch');r.onsuccess=()=>resolve(r.result);r.>reject(r.error);});
bench.dom=document.createElement('div');bench.dom.style.cssText='position:absolute;left:-10000px;top:0;width:300px;contain:strict;height:100px;';
bench.dom.textContent='benchmark';document.body.append(bench.dom);return 1;
};
bench.nativeSync=id=>{switch(id){
case 'external_js_sum':return bench.sum(bench.ints);
case 'external_wasm_sum':return bench.wasm.sum(bench.ints.length);
case 'external_simd_sum':return bench.wasm.simd(bench.ints.length);
case 'external_js_i64':return bench.i64js(10000);
case 'external_wasm_i64':return bench.wasm.i64mix(10000);
case 'dom_layout':{let sum=0;for(let i=0;i<100;i++){bench.dom.style.width=`${200+(i%17)}px`;sum+=bench.dom.offsetWidth;}return sum;}
default:throw Error('Unknown synchronous case '+id);
}};
bench.nativeAsync=async id=>{switch(id){
case 'webcrypto':return [...new Uint8Array(await crypto.subtle.digest('SHA-256',bench.bytes))].reduce((a,b)=>a+b,0);
case 'timer_wait':await new Promise(r=>setTimeout(r,15));return 15;
case 'local_fetch':{const r=await fetch('payload.bin',{cache:'no-store'});if(!r.ok)throw Error('fetch '+r.status);return bench.sum(new Uint8Array(await r.arrayBuffer()));}
case 'indexeddb':return await new Promise((resolve,reject)=>{const tx=bench.db.transaction('scratch','readwrite');const store=tx.objectStore('scratch');
store.put(bench.bytes,'sample');let sum;store.get('sample').onsuccess=e=>{sum=bench.sum(e.target.result);};store.delete('sample');
tx.oncomplete=()=>resolve(sum);tx.>reject(tx.error);tx.onabort=()=>reject(tx.error);});
case 'worker_transfer':return await new Promise((resolve,reject)=>{const copy=bench.bytes.slice().buffer;bench.worker.onmessage=e=>resolve(e.data);bench.worker.
default:throw Error('Unknown async case '+id);
}};
onmessage = ({data}) => {
let sum=0; for(const v of new Uint32Array(data)) sum+=v;
postMessage(sum);
};
(module
(memory (export "memory") 64)
(func (export "sum") (param $n i32) (result i32)
(local $p i32) (local $s i32)
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $p) (local.get $n)))
(local.set $s (i32.add (local.get $s) (i32.load (i32.shl (local.get $p) (i32.const 2)))))
(local.set $p (i32.add (local.get $p) (i32.const 1)))
(br $loop))) (local.get $s))
;; n must be a multiple of four. Tests cover n=0,4,8 and full input.
(func (export "simd") (param $n i32) (result i32)
(local $p i32) (local $s v128)
(local.set $s (v128.const i32x4 0 0 0 0))
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $p) (local.get $n)))
(local.set $s (i32x4.add (local.get $s) (v128.load (i32.shl (local.get $p) (i32.const 2)))))
(local.set $p (i32.add (local.get $p) (i32.const 4)))
(br $loop)))
(i32.add (i32.add (i32x4.extract_lane 0 (local.get $s)) (i32x4.extract_lane 1 (local.get $s)))
(i32.add (i32x4.extract_lane 2 (local.get $s)) (i32x4.extract_lane 3 (local.get $s)))))
(func (export "i64mix") (param $n i32) (result i32)
(local $i i32) (local $x i64)
(local.set $x (i64.const 1234567))
(block $end (loop $loop
(br_if $end (i32.ge_u (local.get $i) (local.get $n)))
(local.set $x (i64.xor (i64.mul (local.get $x) (i64.const 6364136223846793005)) (i64.extend_i32_u (local.get $i))))
(local.set $x (i64.xor (local.get $x) (i64.shr_u (local.get $x) (i64.const 33))))
(local.set $i (i32.add (local.get $i) (i32.const 1))) (br $loop)))
(i32.and (i32.wrap_i64 (local.get $x)) (i32.const 2147483647)))
)
<!DOCTYPE html><html lang="ru"><head>
<base href="$FLUTTER_BASE_HREF"><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WASM versus JavaScript</title>
<style>html,body{margin:0;background:#0b1220;color:#e2e8f0;font:16px system-ui}#loading{padding:32px}</style>
</head><body><div id="loading">WASM versus JavaScript · загрузка</div>
<script src="bench.js"></script><script src="flutter_bootstrap.js" async></script>
</body></html>
{{flutter_js}}
{{flutter_build_config}}
const config = {
canvasKitBaseUrl: 'canvaskit/',
forceSingleThreadedSkwasm: new URLSearchParams(location.search).has('single'),
};
_flutter.loader.load({config});
const http=require('node:http'),fs=require('node:fs'),path=require('node:path');
const root=path.resolve(__dirname,'..');const port=Number(process.env.PORT||8096);
const types={'.html':'text/html; charset=utf-8','.js':'text/javascript','.mjs':'text/javascript','.wasm':'application/wasm','.json':'application/json','.ttf':'font/ttf','.otf':'font/otf','.png':'image/png','.bin':'application/octet-stream'};
http.createServer((req,res)=>{
if(req.url==='/favicon.ico'){res.writeHead(204);res.end();return;}
const url=new URL(req.url,'http://localhost');let name=decodeURIComponent(url.pathname);
if(name==='/'){res.writeHead(302,{Location:'/js/'});res.end();return;}
if(name.endsWith('/'))name+='index.html';
const rel=/^\/(js|wasm)\//.test(name)?'site'+name:name;
const file=path.resolve(root,'.'+(rel.startsWith('/')?rel:'/'+rel));
if(!file.startsWith(root+path.sep)){res.writeHead(403);res.end();return;}
fs.stat(file,(err,s)=>{
if(err||!s.isFile()){res.writeHead(404);res.end('Not found');return;}
res.writeHead(200,{'Content-Type':types[path.extname(file)]||'application/octet-stream','Content-Length':s.size,
'Cross-Origin-Opener-Policy':'same-origin','Cross-Origin-Embedder-Policy':'require-corp',
'Cross-Origin-Resource-Policy':'same-origin','Cache-Control':/index.html|results|report.html/.test(file)?'no-store':'public, max-age=3600'});
fs.createReadStream(file).pipe(res);
});
}).listen(port,'127.0.0.1',()=>console.log(`Benchmark: http://127.0.0.1:${port}/js/`));
name: wasm_versus_javascript
publish_to: none
description: Independent controlled Flutter compiler and browser workload benchmark.
version: 1.0.0
environment:
sdk: '>=3.13.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
web: 1.1.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
Все Dart-файлы приложения, браузерная часть, Wasm-модуль и конфигурация приведены выше целиком. Входные массивы формируются из фиксированного начального значения, поэтому при повторении тестов сохраняется одинаковая нагрузка.
В квадратных скобках показан 95%-й доверительный интервал. Для практического вывода выбран порог 10%: преимущество засчитывается, когда весь интервал выходит за соответствующую границу. Интервал внутри полосы означает близкую скорость. Пограничный результат означает пересечение порога; широкий интервал показывает изменчивость между запусками.
Что получилось в целомВ основном сравнении JavaScript и Wasm ST из 41 задачи Wasm выиграл 10, JavaScript — 12, ещё 12 дали близкую скорость. Семь случаев я проверил расширенной серией, выше. Пять дополнительных задач исследуют отдельные JavaScript- и Wasm-модули.
Самый наглядный результат относится к интерфейсу с 600 ячейками. Подготовка кадра сократилась с 15,31 до 7,92 мс. При этом длительность всей серии обновлений уменьшилась с 427,88 до 410,51 мс, то есть на 4,1%. Время между кадрами продолжает задавать темп серии, а более быстрая подготовка освобождает запас внутри кадра.
В вычислениях результаты разнообразнее: рекурсивные вызовы ускорились у Wasm примерно в 3,89 раза, преобразование текста в UTF-8 — в 3,08 раза. JavaScript быстрее вычислил сумму произведений дробных чисел и восстановил текст из UTF-8. Поэтому полезно рассматривать конкретный путь обработки данных.
Итоги расширенной проверкиДополнительная серия завершена для всех семи выбранных задач. В итоговый набор вошли 36 сессий Chrome, 5 292 учитываемых пакета и 3 780 предварительных пакетов. Один блок из трёх режимов повторён после сбоя проверки ответа; его первоначальные значения сохранены в архиве. Таблица фиксирует полученный итог, включая случаи с сохраняющимся разбросом. Изменчивость измерений — часть наблюдаемого поведения данной нагрузки.
В расширенной серии обмен с Worker, раскладка текста и чтение малого массива показали близкую скорость. JavaScript выиграл зависимое чтение памяти и создание цепочки объектов. Сортировка дала результат около порога 10%, а тест условных переходов сохранил широкий разброс между запусками.
Задача | Итог расширенной серии | JS / Wasm ST · 95% |
|---|---|---|
Скорость практически одинакова. | 0,96× [0,93; 0,97] | |
Скорость практически одинакова. | 1,06× [1,05; 1,07] | |
Скорость заметно меняется между запусками. | 0,81× [0,71; 1,00] | |
JavaScript быстрее в 1,17 раза. | 0,85× [0,72; 0,91] | |
Разница находится около принятого порога 10%. | 0,91× [0,89; 0,93] | |
JavaScript быстрее в 1,13 раза. | 0,88× [0,88; 0,90] | |
Скорость практически одинакова. | 0,97× [0,96; 1,00] |
В общем по производительности Flutter-Web приложений: в усредненном приближении WASM не дает выигрыша во всём, поэтому если нет приоритетов конкретных кейсов использования, то компиляция в JavaScript вполне отличный вариант на текущий момент.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | WASM кроссбраузерность во Flutter Web на 2026 | 0 | 8.01 | 08-10-2026 |
| 2 | Как мы ускоряли прошивку ESP (спойлер: совы не то, чем кажутся) | 0 | 9.66 | 23-07-2026 |
| 3 | [Перевод] Жаргон функционального программирования | 1 | 11.87 | 15-10-2026 |
| 4 | Добавили потоков, стало медленнее: разбираемся с ложным разделением кеш‑линии | 0 | 9.26 | 10-08-2026 |
| 5 | Playwright не спасает от флапающих тестов: разбираемся, как он ждёт на самом деле | 0 | 8.65 | 24-07-2026 |
| 6 | Бенчмаркая try/finally: один finally — и метод в 6,45 раза медленнее | 0 | 8.98 | 17-08-2026 |
| 7 | Бенчмаркая Sum: ускорил циклом — замедлил в ×4,7 | 0 | 7.81 | 18-07-2026 |
| 8 | [Перевод] Почему в Chrome маленькие JPEG выглядят иначе | 0 | 5.9 | 18-08-2026 |
| 9 | vm5277: Java-синтаксис и ООП для 8-бит МК без оверхеда | 0 | 11.05 | 31-08-2026 |
| 10 | Двойная точность: в основном «найти и заменить», плюс два настоящих сюрприза | 1 | 9.12 | 25-08-2026 |