Когда Flutter на WebAssembly быстрее JavaScript: разберем 46 тестов.

Когда мы апгрейдим процессор или видеокарту и начинаем играть в привычную нам игрушку с новым железом, то увеличение производительности на некоторое количество процентов становится сразу же заметным. Например, увеличение производительности заметно уже с 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. Вычисления: числа, матрицы, изображения и звук

  2. Память, объекты и коллекции

  3. Текст и обмен данными

  4. Вызовы JavaScript и асинхронный код

  5. Работа, которую выполняет браузер

  6. Flutter: текст, виджеты, прокрутка и графика

  7. Отдельные вычислительные модули

Плавность и время кадра · Запуск приложения · Практические выводы · Подробности измерений · Сводная таблица · Полные исходники

Вычисления: числа, матрицы, изображения и звук

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),
    )

2. Перемножение и сложение дробных чисел

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

В тесте перемножаются 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),
    )

3. Синусы, косинусы и квадратные корни

Синусы и косинусы встречаются в анимации, геометрии и обработке сигналов. Квадратный корень нужен, например, при вычислении расстояния.

Для каждого из 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;
      },
    )

4. Умножение матриц

Умножение матриц встречается в преобразованиях координат и численных алгоритмах. В этом опыте используются две квадратные матрицы размером 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),
    )

5. Фильтр изображения

Простой способ сгладить изображение — заменить яркость пикселя средним значением его соседей. Здесь для каждой внутренней точки берётся квадрат 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),
    )

6. Фильтр звукового сигнала

При фильтрации звука текущий отсчёт можно вычислять как взвешенную сумму предыдущих. Это основа 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;
      },
    )

7. Вычисления с условиями

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

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;
      },
    )

8. Многократные вложенные вызовы функции

Рекурсивная функция вызывает сама себя. Подобная структура встречается при обходе дерева и разборе вложенных выражений. Для создания большого числа вызовов выбран рекурсивный 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),
    )

9. Арифметика с очень большими целыми числами

Обычных чисел бывает мало, когда нужна точная арифметика с длинными целыми. 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();
      },
    )

10. Та же сумма обычным циклом

Этот тест измеряет сложение массива целых чисел обычным циклом.

Массив содержит 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;
    })

11. Сложение чисел группами по четыре

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;
    })

Память, объекты и коллекции

12. Чтение небольшого массива из памяти

Небольшие массивы встречаются в локальных таблицах и временных буферах. Здесь проверяется последовательное чтение 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;
        },
      )

13. Чтение большого массива из памяти

Тот же последовательный проход выполнен по более крупному массиву: 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;
        },
      )

14. Чтение памяти в непоследовательном порядке

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

В массиве размером 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;
      },
    )

15. Копирование массива

Перед отправкой или обработкой данных приложение часто копирует буфер. Проверяем копирование 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];
      },
    )

16. Создание временных объектов

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

Тест создаёт 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;
      },
    )

17. Создание цепочки объектов и проход по ней

Связанные списки и деревья хранят ссылки на другие объекты. Чтобы проверить этот механизм, создаётся цепочка из 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;
      },
    )

18. Вызов методов разных типов объектов

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

Тест 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;
      },
    )

19. Сортировка

Сортировка нужна спискам, таблицам и результатам поиска. Для опыта взяты 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;
      },
    )

20. Поиск записи по строковому ключу

Типичный пример — найти запись по строковому идентификатору. В 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;
      },
    )

Текст и обмен данными

21. Сборка текста из фрагментов

В журнале событий или экспорте приходится собирать строку из множества фрагментов. Для этой задачи используется 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;
      },
    )

22. Преобразование текста в байты

Перед сохранением текста или отправкой по сети строку часто переводят в байты 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),
    )

23. Преобразование байтов обратно в текст

После получения файла или сетевого ответа байты 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,
    )

24. Поиск в тексте по шаблону

Из текста журнала нужно извлечь номера записей вида 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;
    })

25. Подготовка JSON для отправки или сохранения

Перед отправкой набора записей в 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,
    )

26. Чтение полученного JSON

При открытии списка приложение получает 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;
      },
    )

Вызовы JavaScript и асинхронный код

27. Много маленьких обращений из Dart к JavaScript

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;
    })

28. Передача массива из Dart в JavaScript

Второй вариант интеграции передаёт сразу весь массив одним вызовом. 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))

29. Передача массива в JavaScript и обратно

Иногда после обработки массива 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;
    })

30. Цепочка асинхронных продолжений

Асинхронная функция может выполнять множество коротких продолжений даже при уже готовых данных. Это бывает в цепочках 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;
    })

Работа, которую выполняет браузер

31. Вычисление контрольной суммы средствами браузера

Для проверки целостности данных браузер умеет вычислять 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);
JavaScript-часть, которую вызывает Dart
  case 'webcrypto':return [...new Uint8Array(await crypto.subtle.digest('SHA-256',bench.bytes))].reduce((a,b)=>a+b,0);

32. Ожидание таймера

Приложение запрашивает задержку, после которой продолжает выполнение. Проверяем обычный таймер на 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);
JavaScript-часть, которую вызывает Dart
  case 'timer_wait':await new Promise(r=>setTimeout(r,15));return 15;

33. Получение данных с локального сервера

Для просмотра влияния сборки на получение данных загружается небольшой двоичный файл с локального сервера.

Запрос 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);
JavaScript-часть, которую вызывает Dart
  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()));}

34. Работа с локальным хранилищем браузера

Локальный кэш веб-приложения может хранить буферы в 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);
JavaScript-часть, которую вызывает Dart
  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);});

35. Обмен данными с фоновым обработчиком

Фоновый 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);
JavaScript-часть, которую вызывает Dart
  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);
};

36. Изменение обычной HTML-страницы

При работе с обычной 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))
JavaScript-часть, которую вызывает Dart
  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: текст, виджеты, прокрутка и графика

37. Раскладка текста во Flutter

В текстовом редакторе или списке сообщений 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;
    })

38. Обновление экрана с 600 ячейками

Представим плотную панель из 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'),
      ]));
    }
  }

39. Прокрутка большого списка

В длинном списке важно, сколько работы требуется при смене видимой области. Используется 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'),
      ]));
    }
  }

40. Рисование множества фигур

Диаграмма или визуализация может рисовать тысячи простых фигур каждый кадр. Для такого случая выбран 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;
}

41. Отрисовка с обрезкой и промежуточными слоями

Полупрозрачность, скруглённая обрезка и промежуточные слои усложняют отрисовку. Здесь эти операции повторяются для 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);
JavaScript-часть, которую вызывает Dart
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);

43. Суммирование массива в отдельном Wasm-модуле

Теперь ту же сумму вычисляет небольшой модуль 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);
JavaScript-часть, которую вызывает Dart
  case 'external_wasm_sum':return bench.wasm.sum(bench.ints.length);
Код измеренного WebAssembly-модуля
(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)))
)

44. Суммирование массива с SIMD

Третий вариант складывает числа группами по четыре с помощью 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);
JavaScript-часть, которую вызывает Dart
  case 'external_simd_sum':return bench.wasm.simd(bench.ints.length);
Код измеренного WebAssembly-модуля
(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)))
)

45. Точные 64-битные вычисления в JavaScript

Для точных 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);
JavaScript-часть, которую вызывает Dart
  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);

46. Точные 64-битные вычисления в Wasm

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);
JavaScript-часть, которую вызывает Dart
  case 'external_wasm_i64':return bench.wasm.i64mix(10000);
Код измеренного WebAssembly-модуля
(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)))
)

Почему ускорение Dart и скорость интерфейса различаются

В сценариях интерфейса я измерял две вещи: сколько длится вся серия из 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]

Обычный цикл и Dart Int32x4 внутри одной сборки

Векторный код быстрее работал после перехода с 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

5ca210815161b6d95c3cb6f70ed9aab24e199fb2f739477d1fd1e0a48c0e53ba

site/wasm/main.dart.wasm

1,802,312

8436d1a93c45d27c365d141b5c0beeff4caa53d4b9eb7a371138102761d7abd2

site/wasm/main.dart.mjs

30,966

a6a992033cc9d2d944baf0813d119c88d185043955686028ce28caa4f643028e

Что это означает для разработки

Для обычного интерфейса 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 раза.

Подготовка JSON для отправки или сохранения

0,470

0,340

0,341

1,38× [1,36; 1,40]

1,38× [1,37; 1,38]

Wasm быстрее в 1,38 раза.

Чтение полученного JSON

0,211

0,321

0,325

0,65× [0,65; 0,66]

0,65× [0,65; 0,66]

JavaScript быстрее в 1,53 раза.

Много маленьких обращений из Dart к JavaScript

0,006

0,197

0,197

0,03× [0,03; 0,03]

0,03× [0,03; 0,03]

JavaScript быстрее в 33,52 раза.

Передача массива из Dart в JavaScript

0,079

0,155

0,155

0,51× [0,50; 0,52]

0,51× [0,50; 0,51]

JavaScript быстрее в 1,98 раза.

Передача массива в JavaScript и обратно

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%.

Изменение обычной HTML-страницы

0,576

0,553

0,549

1,04× [1,00; 1,06]

1,05× [1,04; 1,06]

Скорость практически одинакова.

Раскладка текста во Flutter

3,473

3,226

3,194

1,08× [1,05; 1,13]

1,09× [1,04; 1,15]

Разница находится около принятого порога 10%.

Обновление экрана с 600 ячейками

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]

Скорость практически одинакова.

Суммирование массива в отдельной JavaScript-функции

0,261

0,260

0,259

1,00× [0,99; 1,01]

1,00× [0,99; 1,02]

Отдельный модуль: сравнение внутри кейса

Суммирование массива в отдельном Wasm-модуле

0,070

0,069

0,069

1,01× [0,99; 1,03]

1,01× [0,99; 1,02]

Отдельный модуль: сравнение внутри кейса

Суммирование массива с SIMD

0,031

0,032

0,031

0,99× [0,94; 1,02]

0,99× [0,98; 1,01]

Отдельный модуль: сравнение внутри кейса

Точные 64-битные вычисления в JavaScript

0,181

0,179

0,180

1,01× [0,99; 1,02]

1,01× [0,99; 1,02]

Отдельный модуль: сравнение внутри кейса

Точные 64-битные вычисления в Wasm

0,014

0,014

0,014

0,99× [0,98; 1,00]

0,99× [0,97; 1,00]

Отдельный модуль: сравнение внутри кейса

Полный код приложения

В приложении используются четыре Dart-файла: вычислительные задачи, измерительный цикл, экран Flutter и объявления взаимодействия с JavaScript. Ниже — их полный код вместе с браузерными обработчиками и конфигурацией.

Входные данные и вычислительные задачи — kernels.dart
import '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'};
}
Измерительный цикл — benchmark.dart
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;
}
Flutter-приложение — main.dart
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;
}
Взаимодействие Dart и JavaScript — bridge.dart
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);
Проверки вычислений — kernels_test.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);}
  });
}
Браузерные обработчики — bench.js
'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);
}};
Фоновый обработчик — worker.js
onmessage = ({data}) => {
  let sum=0; for(const v of new Uint32Array(data)) sum+=v;
  postMessage(sum);
};
Вычислительные модули WebAssembly — kernels.wat
(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)))
)
Страница запуска — index.html
<!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 — flutter_bootstrap.js
{{flutter_js}}
{{flutter_build_config}}
const config = {
  canvasKitBaseUrl: 'canvaskit/',
  forceSingleThreadedSkwasm: new URLSearchParams(location.search).has('single'),
};
_flutter.loader.load({config});
Локальный сервер — serve.cjs
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/`));
Зависимости Flutter — pubspec.yaml
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]

Раскладка текста во Flutter

Скорость практически одинакова.

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 вполне отличный вариант на текущий момент.

Комментарии (3)


  1. Octagon77
    09.10.2026 20:23

    Труд титанический, результат ожидаемый. Занимался подобным по-лёгкому, пришёл к тому, что

    • JavaScript без указания деталей - профанация, в Сафари одно, в Огнелисе другое.

    • JavaScript и так быстрый, сильно ускорить его невозможно. Даже на десктопе.

    • Если уж ускорять, то на С, а не на Dart, и даже не на С++, и уж тем более не на Go.

    • Выходит, WebAssembly - это для переноса кодовой базы, якобы "надёжности" типа Rust, обфускации, кроссплатформы десктопа и браузера может быть, ещё придумать чего, а для скорости - в последнюю очередь.


    1. lil_master Автор
      09.10.2026 20:23

      Я бы сказал так - всё зависит от кейсов, в веб приложении, в некоторых кейсах однозначно WASM бьёт, но это узкие направления.

      И, думаю, WASM на dart и WASM на C - это один и тот же WASM, т е профита не даст, но хорошо бы это проверить.

      А насчёт надёжности, вряд-ли типа Rust, скорее стабильность кадров отрисовки в WASM лучше, т е именно производительность на один кадр, но не думаю, что он очень безопасный по памяти как Rust, хотя можно писать через Rust-Dart bridge или напрямую Rust->WASM.


    1. supercat1337
      09.10.2026 20:23

      Ну, и если сравнить вес бандла esm и wasm, то тоже не сильно в пользу последнего.