Многопоточность и Web Workers

Криптографические операции в JavaScript часто оказываются вычислительно тяжёлыми. Даже такие базовые алгоритмы, как SHA-256, HMAC или PBKDF2, при обработке больших объёмов данных могут блокировать основной поток выполнения браузера. Это приводит к «замораживанию» интерфейса: перестают реагировать кнопки, анимации и ввод пользователя. Причина проста — JavaScript в браузере по умолчанию однопоточен, и любые долгие вычисления выполняются синхронно.

Библиотека CryptoJS реализована на чистом JavaScript и не использует нативные расширения. Это делает её универсальной, но увеличивает нагрузку на основной поток при интенсивных вычислениях.

Типичные сценарии, где возникает проблема производительности:

  • хеширование больших файлов (десятки и сотни мегабайт);
  • многократное вычисление HMAC в реальном времени;
  • генерация ключей через PBKDF2 с большим числом итераций;
  • обработка потоковых данных (например, логов или сетевых пакетов).

T(n) = O(n k)

где n — размер данных, а k — число итераций алгоритма (например, PBKDF2). При росте любого из параметров время выполнения становится критичным для UI-потока.

В браузере существует ограничение: один поток отвечает за DOM, события и выполнение JS. Если криптографическая функция занимает даже 200–300 мс, интерфейс уже ощущается «дерганым». Поэтому оптимальная архитектура требует выноса вычислений в Web Workers.


Модель многопоточности в браузере

Web Workers предоставляют механизм фонового выполнения JavaScript-кода в отдельном потоке. Они не имеют доступа к DOM, но могут обмениваться сообщениями с основным потоком через postMessage.

Основные характеристики:

  • изоляция памяти (нет shared state по умолчанию);
  • асинхронная коммуникация;
  • отсутствие блокировки UI;
  • возможность параллелизации задач.

Архитектурно это выглядит так:

  • главный поток: интерфейс, ввод, управление задачами;
  • worker-поток: криптография, вычисления, обработка данных.

Базовая интеграция CryptoJS с Web Worker

Минимальная схема использования выглядит следующим образом.

Worker-файл (crypto-worker.js)

importScripts('crypto-js.js');

self.onmess age = function (e) {
    const { type, payload } = e.data;

    if (type === 'SHA256') {
        const hash = CryptoJS.SHA256(payload).toString();
        self.postMessage({ type: 'RESULT', hash });
    }

    if (type === 'HMAC') {
        const { message, key } = payload;
        const result = CryptoJS.HmacSHA256(message, key).toString();
        self.postMessage({ type: 'RESULT', result });
    }
};

Основной поток

const worker = new Worker('crypto-worker.js');

worker.onmess age = function (e) {
    console.log('Результат:', e.data);
};

worker.postMessage({
    type: 'SHA256',
    payload: 'Hello World'
});

Потокобезопасная архитектура криптографических задач

При проектировании системы важно учитывать не только перенос вычислений, но и структуру задач.

1. Разделение задач на единицы работы

Крупные операции следует дробить:

  • файл → чанки;
  • поток данных → блоки фиксированного размера;
  • массив операций → очередь задач.

Это позволяет распределять нагрузку между несколькими worker-ами.

2. Worker Pool

Один worker — ограничение по параллелизму. Для реальной нагрузки применяется пул:

  • 2–8 worker-ов в зависимости от CPU;
  • распределение задач через очередь;
  • балансировка нагрузки.

Пример простой реализации пула:

class WorkerPool {
    constructor(size, script) {
        this.workers = Array.from({ length: size }, () => new Worker(script));
        this.queue = [];
        this.index = 0;
    }

    exec(task) {
        return new Promise((resolve) => {
            const worker = this.workers[this.index];
            this.index = (this.index + 1) % this.workers.length;

            worker.onmess age = (e) => resolve(e.data);
            worker.postMessage(task);
        });
    }
}

Хеширование больших данных

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

Потоковое хеширование через worker

importScripts('crypto-js.js');

let sha = CryptoJS.algo.SHA256.create();

self.onmess age = function (e) {
    const { chunk, isFinal } = e.data;

    sha.update(CryptoJS.enc.Utf8.parse(chunk));

    if (isFinal) {
        const hash = sha.finalize().toString();
        self.postMessage({ hash });
        sha = CryptoJS.algo.SHA256.create();
    }
};

Такой подход позволяет:

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

PBKDF2 и изоляция вычислений

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

T = c h(n)

где:

  • c — число итераций,
  • h(n) — стоимость хеш-функции.

При значениях итераций 100000+ выполнение в UI-потоке недопустимо.

Worker-реализация:

importScripts('crypto-js.js');

self.onmess age = function (e) {
    const { password, salt, iterations } = e.data;

    const key = CryptoJS.PBKDF2(password, salt, {
        keySize: 256 / 32,
        iterations
    });

    self.postMessage({ key: key.toString() });
};

Передача данных между потоками

Web Workers используют структурированное клонирование. Это означает:

  • данные копируются, а не передаются по ссылке;
  • большие объекты создают overhead;
  • бинарные данные лучше передавать через ArrayBuffer.

Оптимизация:

worker.postMessage(buffer, [buffer]);

Это позволяет использовать transferables без копирования памяти.


Узкие места при использовании CryptoJS в workers

Несмотря на вынесение в отдельный поток, остаются ограничения:

1. Отсутствие SIMD и WebCrypto ускорения

CryptoJS полностью интерпретируемый, поэтому уступает WebCrypto API.

2. Стоимость сериализации сообщений

Передача больших строк или объектов может стать bottleneck.

3. Garbage Collection внутри worker

При частых вызовах возможны паузы GC.


Сравнение с Web Crypto API

Web Crypto API выполняется нативно и часто быстрее:

  • SHA-256: аппаратное ускорение;
  • AES: использование CPU инструкций;
  • PBKDF2: оптимизированная реализация.

Однако Web Crypto:

  • менее гибкий;
  • не всегда поддерживает нужные режимы;
  • сложнее интегрируется в кастомные алгоритмы.

Поэтому CryptoJS в Web Workers используется там, где нужна гибкость.


Паттерн асинхронного криптографического сервиса

Часто строится абстракция уровня сервиса:

  • enqueue задач;
  • распределение по worker-ам;
  • возврат Promise;
  • контроль отмены.

Пример интерфейса:

cryptoService.sha256(data)
cryptoService.hmac(message, key)
cryptoService.pbkdf2(password, salt, iterations)

Внутри — очередь задач и worker pool.


Отмена вычислений

Web Workers можно завершать принудительно:

worker.terminate();

Но это грубый способ. Более корректный подход:

  • флаг отмены в сообщениях;
  • проверка внутри worker;
  • прерывание вычислений между блоками.

Практические рекомендации архитектуры

При проектировании криптографического слоя с CryptoJS и Web Workers важно учитывать:

  • не отправлять слишком мелкие задачи (накладные расходы больше пользы);
  • агрегировать операции;
  • использовать pool вместо одного worker;
  • минимизировать передачу строк, использовать бинарные буферы;
  • разделять CPU-bound и IO-bound операции;
  • избегать синхронного CryptoJS в UI полностью.

Типовая структура приложения

UI Layer
   ↓
Task Queue
   ↓
Worker Pool
   ↓
CryptoJS Engine (inside workers)
   ↓
Results Aggregation

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