Бенчмаркинг алгоритмов в браузере

Бенчмаркинг криптографических алгоритмов в JavaScript требует учёта нескольких фундаментальных факторов: нестабильности среды выполнения, влияния движка (V8, SpiderMonkey, JavaScriptCore), а также особенностей работы сборщика мусора. Любые измерения, выполненные без учёта этих факторов, дают искажённую картину производительности.

При работе с библиотекой Crypto-js ключевая сложность заключается в том, что реализация полностью выполнена на чистом JavaScript и не использует нативные ускорения Web Crypto API. Это делает её удобной для кросс-браузерных тестов, но чувствительной к оптимизациям движка.

Базовая методика измерения времени выполнения

Основной инструмент измерения в браузере — performance.now(). Он обеспечивает высокую точность по сравнению с Date.now() и подходит для микробенчмарков.

function benchmark(fn, iterations = 1000) {
  const start = performance.now();

  for (let i = 0; i < iterations; i++) {
    fn();
  }

  const end = performance.now();
  return end - start;
}

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

Прогрев (warm-up) функций

Перед измерением необходимо выполнить несколько «холостых» прогонов:

function warmup(fn, iterations = 100) {
  for (let i = 0; i < iterations; i++) {
    fn();
  }
}

Только после этого можно переходить к измерению.

Бенчмаркинг хеш-функций Crypto-js

Crypto-js предоставляет несколько популярных алгоритмов хеширования: MD5, SHA-1, SHA-256 и SHA-512. Их производительность существенно различается из-за разной сложности внутренних операций.

Пример измерения SHA-256

const input = "benchmark test string";

function sha256Test() {
  return CryptoJS.SHA256(input).toString();
}

warmup(sha256Test);

const time = benchmark(sha256Test, 5000);
console.log("SHA-256:", time, "ms");

Сравнение алгоритмов

При одинаковом объёме входных данных наблюдается закономерность:

  • MD5 — минимальная вычислительная нагрузка
  • SHA-1 — умеренная нагрузка
  • SHA-256 — существенно выше
  • SHA-512 — наиболее тяжёлый в чистом JS исполнении

Причина кроется в количестве раундов обработки и размере внутренних блоков.

Влияние длины входных данных

Производительность Crypto-js нелинейно зависит от размера строки. При увеличении входных данных происходит рост затрат на:

  • разбиение на блоки
  • копирование массивов WordArray
  • конвертацию UTF-8

Для корректного анализа используется параметризированный тест:

function generateString(size) {
  return new Array(size).fill("A").join("");
}

const sizes = [10, 100, 1000, 5000];

sizes.forEach(size => {
  const input = generateString(size);

  const fn = () => CryptoJS.SHA256(input).toString();

  warmup(fn);
  const time = benchmark(fn, 1000);

  console.log(size, "bytes:", time, "ms");
});

Особенности работы с WordArray

Crypto-js использует внутренний формат WordArray, который представляет данные как массив 32-битных слов. Это влияет на производительность при частых преобразованиях строк.

Узкое место: конвертация строк

CryptoJS.enc.Utf8.parse(input)

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

Для честного бенчмарка необходимо разделять этапы:

  • преобразование входа
  • вычисление хеша
  • преобразование результата

Бенчмаркинг HMAC

HMAC добавляет дополнительный слой обработки поверх базовой хеш-функции. Это увеличивает количество операций и усложняет сравнение.

function hmacTest() {
  return CryptoJS.HmacSHA256("data", "key").toString();
}

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

PBKDF2 как стресс-тест

PBKDF2 в Crypto-js используется для растяжения паролей и является одним из самых дорогих по вычислениям алгоритмов.

function pbkdf2Test() {
  return CryptoJS.PBKDF2("password", "salt", {
    keySize: 256 / 32,
    iterations: 1000
  }).toString();
}

Влияние количества итераций

T(n)=n T_{hash} + T_{setup}

Линейная зависимость от числа итераций делает PBKDF2 удобным инструментом для нагрузочного тестирования криптографического слоя.

Сравнение стабильности результатов

При бенчмаркинге в браузере наблюдается разброс значений из-за:

  • фоновой работы GC
  • конкуренции с UI-потоком
  • турбо-оптимизаций движка
  • JIT-деоптимизаций

Для повышения точности применяют медиану вместо среднего:

function benchmarkMedian(fn, runs = 10) {
  const results = [];

  for (let i = 0; i < runs; i++) {
    warmup(fn);
    results.push(benchmark(fn, 1000));
  }

  results.sort((a, b) => a - b);
  return results[Math.floor(results.length / 2)];
}

Кэширование и влияние повторных вызовов

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

Для исключения эффекта кэширования используют динамические строки:

let counter = 0;

function dynamicInput() {
  return "data_" + (counter++);
}

function test() {
  return CryptoJS.SHA256(dynamicInput()).toString();
}

Сравнение Crypto-js и Web Crypto API

Хотя Crypto-js удобен для тестирования, его производительность существенно уступает нативному API.

Основные различия:

  • Web Crypto использует нативные реализации (C/C++)
  • Crypto-js работает в интерпретируемом JS
  • Web Crypto выполняется асинхронно
  • Crypto-js полностью синхронен

Это делает результаты бенчмарков Crypto-js полезными только для сравнений внутри самой библиотеки, но не для абсолютной оценки криптографической производительности браузера.

Ошибки при проведении измерений

Часто встречающиеся проблемы:

  • отсутствие прогрева функций
  • использование Date.now() вместо performance.now()
  • слишком малое число итераций
  • измерение вместе с генерацией входных данных
  • игнорирование влияния GC

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

Практическая модель оценки производительности

Для систематического анализа используют агрегированную модель:

T_{total} = T_{input} + T_{crypto} + T_{output}

Где каждый компонент измеряется отдельно, чтобы исключить перекрытия.

Поведение при масштабировании нагрузки

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

  • деградация производительности из-за однопоточности JS
  • рост задержек UI
  • увеличение времени GC-пауз
  • нестабильность результатов измерений

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

Оптимизация тестового стенда

Для получения более стабильных результатов:

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