← Назад к вопросам

Как сделать чтобы сложная математическая функция не занимала много процессов в Event Loop?

3.0 Senior🔥 111 комментариев
#Node.js и JavaScript#Кэширование и производительность

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

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как сделать чтобы сложная математическая функция не занимала много процессов в Event Loop?

Это критически важный вопрос для Node.js. Event Loop в Node.js однопоточный, и если мы блокируем его тяжелыми вычислениями, все остальные запросы будут ждать. Расскажу про решения.

Проблема: CPU-Bound операции блокируют Event Loop

Демонстрация проблемы:

// app.js - однопоточный сервер
const express = require('express');
const app = express();

// Тяжелая математическая функция
function complexCalculation(n) {
  let result = 0;
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      result += Math.sqrt(i * j);
    }
  }
  return result;
}

// Endpoint, который выполняет сложные вычисления
app.get('/calculate/:n', (req, res) => {
  const n = parseInt(req.params.n);
  const result = complexCalculation(n); // Блокирует Event Loop!
  res.json({ result });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Что происходит:

Время 0 мс:    Клиент 1 отправляет GET /calculate/5000
Время 10 мс:   Server начинает вычисление
Время 100 мс:  Клиент 2 отправляет GET /health (простой запрос)
Время 200 мс:  Клиент 3 отправляет GET /users

Время 5000 мс: Вычисление завершилось
                Теперь обрабатываются /health и /users
                Все запросы ждали 5 секунд!

Это неправильно. Node.js сильнее именно благодаря асинхронности.

Решение 1: Разделение вычислений на микротаски

Используем setImmediate для разделения работы:

// Функция, которая разбивает вычисления на части
function complexCalculationAsync(n) {
  return new Promise((resolve) => {
    let result = 0;
    let i = 0;
    
    function processChunk() {
      // Обработаем только часть данных
      const chunkSize = 100;
      const end = Math.min(i + chunkSize, n);
      
      for (; i < end; i++) {
        for (let j = 0; j < n; j++) {
          result += Math.sqrt(i * j);
        }
      }
      
      if (i < n) {
        // Есть ещё работа, продолжим после других операций
        setImmediate(processChunk);
      } else {
        // Всё готово
        resolve(result);
      }
    }
    
    processChunk();
  });
}

app.get('/calculate/:n', async (req, res) => {
  const n = parseInt(req.params.n);
  const result = await complexCalculationAsync(n); // Не блокирует!
  res.json({ result });
});

Временная шкала:

Время 0 мс:    Клиент 1: GET /calculate/5000
Время 10 мс:   Сервер обрабатывает chunk 1 (100 итераций)
Время 20 мс:   Клиент 2: GET /health → обработан немедленно!
Время 30 мс:   Сервер обрабатывает chunk 2
Время 40 мс:   Клиент 3: GET /users → обработан немедленно!
Время 5000 мс: Вычисление завершилось

Все запросы обрабатываются почти мгновенно!

Решение 2: Worker Threads (для тяжелых вычислений)

Это наиболее эффективное решение для действительно сложных вычислений.

Создаём отдельный worker thread:

// worker.js - выполняет вычисления в отдельном потоке
const { parentPort } = require('worker_threads');

function complexCalculation(n) {
  let result = 0;
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      result += Math.sqrt(i * j);
    }
  }
  return result;
}

// Слушаем сообщения от main thread
parentPort.on('message', (n) => {
  const result = complexCalculation(n);
  // Отправляем результат обратно
  parentPort.postMessage(result);
});

Используем worker threads в main приложении:

// app.js
const express = require('express');
const { Worker } = require('worker_threads');
const path = require('path');
const app = express();

// Pool воркеров (переиспользуемые потоки)
class WorkerPool {
  constructor(workerScript, poolSize = 4) {
    this.workers = [];
    this.queue = [];
    
    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(workerScript);
      worker.on('message', (result) => {
        worker.busy = false;
        const { resolve } = worker.currentTask;
        resolve(result);
        
        // Обработаем следующий запрос из очереди
        if (this.queue.length > 0) {
          this.executeTask(this.queue.shift());
        }
      });
      worker.busy = false;
      this.workers.push(worker);
    }
  }
  
  async executeTask(task) {
    const availableWorker = this.workers.find(w => !w.busy);
    
    if (!availableWorker) {
      // Все воркеры заняты, добавляем в очередь
      return new Promise((resolve) => {
        this.queue.push({ task, resolve });
      });
    }
    
    return new Promise((resolve) => {
      availableWorker.busy = true;
      availableWorker.currentTask = { resolve };
      availableWorker.postMessage(task);
    });
  }
}

const pool = new WorkerPool(path.join(__dirname, 'worker.js'), 4);

app.get('/calculate/:n', async (req, res) => {
  const n = parseInt(req.params.n);
  
  // Вычисления выполняются в отдельном потоке
  const result = await pool.executeTask(n);
  
  res.json({ result });
});

app.listen(3000, () => console.log('Server running'));

Преимущества Worker Threads:

  • CPU-интенсивные операции не блокируют Event Loop
  • Можем использовать всё ядра процессора
  • На многоядерной машине исполняется параллельно

Результат:

Machine with 4 cores + Worker Pool(4)

Без Worker Threads:
Запрос 1: 5000 мс (блокирует всё)
Запрос 2: 10000 мс (ждёт запроса 1)
Запрос 3: 15000 мс (ждёт запросов 1 и 2)

С Worker Threads (4 воркеров):
Запрос 1: 5000 мс (воркер 1)
Запрос 2: 5000 мс (воркер 2, параллельно)
Запрос 3: 5000 мс (воркер 3, параллельно)
Запрос 4: 5000 мс (воркер 4, параллельно)
Запрос 5: 5000 мс (воркер 1, когда освободился)

Решение 3: Используем встроенные оптимизированные функции

Многие операции уже оптимизированы на C++ уровне.

// Плохо: медленное вычисление на JavaScript
function slowSqrt(numbers) {
  return numbers.map(n => {
    let x = n;
    for (let i = 0; i < 100; i++) {
      x = (x + n / x) / 2; // Newton method
    }
    return x;
  });
}

// Хорошо: используем встроенный Math.sqrt (на C++)
function fastSqrt(numbers) {
  return numbers.map(n => Math.sqrt(n));
}

// fastSqrt в 1000+ раз быстрее

Решение 4: WebAssembly (для экстремальных случаев)

Для самых тяжелых вычислений можно использовать WebAssembly.

// app.js
const wasm = require('./calc.wasm');

app.get('/calculate/:n', (req, res) => {
  const n = parseInt(req.params.n);
  // WebAssembly выполняется очень быстро
  const result = wasm.complexCalculation(n);
  res.json({ result });
});

WASM быстрее JavaScript в 10-100 раз для математических операций.

Решение 5: Offload на микросервис

Для очень тяжелых вычислений можно создать отдельный сервис.

// main-api.js (быстрый API сервер)
const express = require('express');
const axios = require('axios');
const app = express();

app.get('/calculate/:n', async (req, res) => {
  const n = req.params.n;
  
  // Отправляем работу в специализированный сервис
  const response = await axios.get(`http://calc-service:3000/compute/${n}`);
  
  res.json(response.data);
});

// calc-service.js (может быть на C++, Go, или специально оптимизирован)
// Получает запросы на вычисления и выполняет их

Решение 6: Кэширование результатов

Часто можно избежать дорогих вычислений кэшированием.

const cache = new Map();

app.get('/calculate/:n', async (req, res) => {
  const n = req.params.n;
  
  // Проверяем кэш
  if (cache.has(n)) {
    return res.json({ result: cache.get(n), cached: true });
  }
  
  // Вычисляем
  const result = await pool.executeTask(n);
  
  // Кэшируем на 1 час
  cache.set(n, result);
  setTimeout(() => cache.delete(n), 3600000);
  
  res.json({ result, cached: false });
});

Полный пример: Оптимизированное решение

const express = require('express');
const { Worker } = require('worker_threads');
const path = require('path');

class SmartCalculator {
  constructor(poolSize = 4) {
    this.workers = [];
    this.cache = new Map();
    this.setupWorkers(poolSize);
  }
  
  setupWorkers(poolSize) {
    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(path.join(__dirname, 'worker.js'));
      worker.busy = false;
      worker.queue = [];
      
      worker.on('message', (result) => {
        worker.busy = false;
        const task = worker.currentTask;
        if (task) {
          task.resolve(result);
        }
        this.processQueue(worker);
      });
      
      this.workers.push(worker);
    }
  }
  
  processQueue(worker) {
    if (worker.queue.length > 0) {
      const task = worker.queue.shift();
      this.executeOnWorker(worker, task);
    }
  }
  
  executeOnWorker(worker, task) {
    worker.busy = true;
    worker.currentTask = task;
    worker.postMessage(task.n);
  }
  
  async calculate(n) {
    // Проверяем кэш
    const cacheKey = `calc_${n}`;
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey);
    }
    
    // Находим свободного воркера
    let availableWorker = this.workers.find(w => !w.busy);
    
    // Если нет свободного, используем очередь
    if (!availableWorker) {
      availableWorker = this.workers[0];
    }
    
    return new Promise((resolve) => {
      const task = { n, resolve };
      
      if (availableWorker.busy) {
        availableWorker.queue.push(task);
      } else {
        this.executeOnWorker(availableWorker, task);
      }
    }).then((result) => {
      // Кэшируем результат
      this.cache.set(cacheKey, result);
      // Удаляем кэш через час
      setTimeout(() => this.cache.delete(cacheKey), 3600000);
      return result;
    });
  }
}

const app = express();
const calc = new SmartCalculator(4);

app.get('/calculate/:n', async (req, res) => {
  try {
    const n = parseInt(req.params.n);
    const result = await calc.calculate(n);
    res.json({ result, status: 'success' });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000, () => console.log('Server running'));

Сравнение подходов

ПодходСложностьПроизводительностьЛучше для
setImmediateНизкая5xЛегкие операции (< 100 мс)
Worker ThreadsСредняя50-100xСредние (100 мс - 5 сек)
WebAssemblyВысокая100-1000xЭкстремальные (> 5 сек)
МикросервисВысокаяЗависитОчень тяжелые + переиспользование
КэшированиеНизкая10000xПовторяющиеся запросы

Лучшие практики

  1. Измеряй: Используй console.time() для профилирования
console.time('calc');
const result = complexCalculation(5000);
console.timeEnd('calc'); // cal: 523.45ms
  1. Выбирай правильный инструмент:

    • < 50 мс: синхронно
    • 50-500 мс: setImmediate
    • 500 мс: Worker Threads

  2. Используй пулы: Не создавай новый Worker для каждого запроса

  3. Кэшируй: Если можешь избежать вычисления — избегай

  4. Мониторь: Отслеживай CPU и памяти

Вывод

Event Loop — сердце Node.js, и блокирование его тяжелыми вычислениями — это смертный грех. Выбери подходящий инструмент, и приложение будет быстрым и отзывчивым для всех пользователей.