Использование Web Workers для вычислений в браузере

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

Web Workers предоставляют изолированную среду исполнения, в которой JavaScript выполняется параллельно с основным потоком. В контексте криптографии это решает сразу несколько задач:

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

TweetNaCl.js хорошо подходит для работы в воркерах, поскольку не зависит от браузерного DOM и не требует глобальных объектов окна.

Архитектура взаимодействия основного потока и Web Worker

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

Типичная схема включает:

  • основной поток: формирует запросы на операции
  • Web Worker: выполняет криптографические функции TweetNaCl
  • канал сообщений: JSON-подобные структуры или бинарные буферы

Внутренний протокол взаимодействия обычно строится вокруг типов операций:

  • generateKeyPair
  • encrypt
  • decrypt
  • sign
  • verify

Базовая структура Web Worker с TweetNaCl.js

Воркер-скрипт инициализируется как отдельный модуль:

// worker.js
importScripts('nacl.min.js');

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

    try {
        let result;

        switch (type) {
            case 'generateKeyPair':
                result = nacl.box.keyPair();
                break;

            case 'sign':
                result = nacl.sign.detached(payload.message, payload.secretKey);
                break;

            case 'verify':
                result = nacl.sign.detached.verify(
                    payload.message,
                    payload.signature,
                    payload.publicKey
                );
                break;

            case 'encrypt':
                result = nacl.box(
                    payload.message,
                    payload.nonce,
                    payload.publicKey,
                    payload.secretKey
                );
                break;

            case 'decrypt':
                result = nacl.box.open(
                    payload.ciphertext,
                    payload.nonce,
                    payload.publicKey,
                    payload.secretKey
                );
                break;

            default:
                throw new Error('Unknown operation');
        }

        self.postMessage({ id, result });
    } catch (e) {
        self.postMessage({ id, error: e.message });
    }
};

Основной поток работает с воркером через идентификаторы задач:

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

function request(type, payload) {
    const id = crypto.randomUUID();

    return new Promise((resolve, reject) => {
        worker.postMessage({ id, type, payload });

        worker.onmess age = (event) => {
            if (event.data.id !== id) return;

            if (event.data.error) {
                reject(event.data.error);
            } else {
                resolve(event.data.result);
            }
        };
    });
}

Работа с бинарными данными

Криптографические операции TweetNaCl.js работают с Uint8Array. Для минимизации накладных расходов используется передача буферов через механизм transferables.

Передача без копирования:

worker.postMessage(
    {
        id,
        type: 'encrypt',
        payload: {
            message: messageBuffer,
            nonce,
            publicKey,
            secretKey
        }
    },
    [messageBuffer.buffer]
);

Использование transferables исключает глубокое копирование данных и существенно ускоряет обработку больших сообщений.

Генерация ключевых пар в отдельном потоке

Операция генерации ключей является одной из самых затратных в криптографии на основе эллиптических кривых.

case 'generateKeyPair':
    const keyPair = nacl.box.keyPair();
    result = {
        publicKey: keyPair.publicKey,
        secretKey: keyPair.secretKey
    };
    break;

Результаты возвращаются в виде Uint8Array, которые затем могут быть сохранены или закэшированы.

Подпись и проверка сообщений

Подпись сообщений выполняется через nacl.sign.detached, что позволяет отделять подпись от данных.

case 'sign':
    result = nacl.sign.detached(payload.message, payload.secretKey);
    break;

Проверка выполняется без раскрытия приватного ключа:

case 'verify':
    result = nacl.sign.detached.verify(
        payload.message,
        payload.signature,
        payload.publicKey
    );
    break;

Операции хорошо параллелятся и масштабируются на несколько воркеров.

Шифрование и дешифрование сообщений

Symmetric-asymmetric гибридный подход в TweetNaCl.js часто реализуется через box.

case 'encrypt':
    result = nacl.box(
        payload.message,
        payload.nonce,
        payload.publicKey,
        payload.secretKey
    );
    break;
case 'decrypt':
    result = nacl.box.open(
        payload.ciphertext,
        payload.nonce,
        payload.publicKey,
        payload.secretKey
    );
    break;

Nonce должен быть уникальным для каждой операции, иначе нарушается криптографическая стойкость.

Организация пула Web Workers

При интенсивных нагрузках один воркер становится недостаточным. Используется пул воркеров:

  • фиксированное количество потоков
  • распределение задач по очереди
  • балансировка нагрузки

Простейшая модель распределения:

class WorkerPool {
    constructor(size) {
        this.workers = Array.from(
            { length: size },
            () => new Worker('worker.js')
        );

        this.queue = [];
        this.index = 0;
    }

    exec(type, payload) {
        const worker = this.workers[this.index];
        this.index = (this.index + 1) % this.workers.length;

        return new Promise((resolve) => {
            const id = crypto.randomUUID();

            worker.onmess age = (event) => {
                if (event.data.id === id) {
                    resolve(event.data.result);
                }
            };

            worker.postMessage({ id, type, payload });
        });
    }
}

Обработка ошибок и прерывание задач

Web Workers не поддерживают отмену вычислений напрямую. Используются следующие подходы:

  • логический флаг отмены задачи
  • пересоздание воркера
  • игнорирование результата по id
if (event.data.cancelled) return;

При критических криптографических операциях предпочтительнее пересоздание воркера.

Производительность и узкие места

Основные ограничения производительности:

  • сериализация сообщений между потоками
  • копирование больших буферов
  • отсутствие SIMD-ускорений в pure JS реализации TweetNaCl.js
  • блокировки при частом создании воркеров

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

  • повторное использование воркеров
  • использование transferables
  • минимизация JSON-структур
  • пакетная обработка сообщений

Особенности использования TweetNaCl.js в воркерах

TweetNaCl.js изначально спроектирован как автономная библиотека без внешних зависимостей, что упрощает интеграцию в Web Worker:

  • отсутствует зависимость от window
  • не требует DOM API
  • работает в строгом контексте выполнения

Однако важно учитывать:

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

Интеграция с современными сборщиками

При использовании Webpack, Vite или Rollup воркеры подключаются как отдельные чанки:

const worker = new Worker(
    new URL('./worker.js', import.meta.url),
    { type: 'module' }
);

TweetNaCl.js может быть включён:

  • как CommonJS модуль
  • как ESM
  • как bundled UMD скрипт

Выбор формата влияет на размер итогового бандла и время инициализации воркера.

Ограничения архитектуры

Использование Web Workers накладывает ряд архитектурных ограничений:

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

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