Приведи пример задачи на производительность в Node.js.

Ответ

Задача: Обработка большого CSV-файла (сотни МБ) с трансформацией данных и записью результата в базу данных.

Наивное (медленное) решение: Загрузка всего файла в память.

const fs = require('fs');
const csv = require('csv-parser');

// Проблема: весь файл загружается в память
fs.readFile('huge-file.csv', 'utf8', (err, data) => {
  if (err) throw err;
  const lines = data.split('n');
  const results = [];

  lines.forEach(line => {
    // Обработка каждой строки
    const processed = transformLine(line);
    results.push(processed);
  });

  // Массив results может занимать гигабайты памяти
  saveToDatabase(results);
});

Оптимизированное решение с использованием Streams и пайплайнов:

const fs = require('fs');
const { pipeline } = require('stream');
const csv = require('csv-parser');
const { Transform } = require('stream');

// Кастомный Transform stream для обработки данных
const transformer = new Transform({
  objectMode: true,
  transform(chunk, encoding, callback) {
    // Обработка одной строки CSV
    const processed = {
      id: chunk.id,
      value: calculateValue(chunk),
      timestamp: new Date()
    };
    callback(null, processed);
  }
});

// Пайплайн: чтение → парсинг CSV → трансформация → запись в БД
pipeline(
  fs.createReadStream('huge-file.csv'),
  csv(),
  transformer,
  async function (source) {
    for await (const chunk of source) {
      // Пакетная запись в БД
      await saveToDatabaseBatch(chunk);
    }
  },
  (err) => {
    if (err) {
      console.error('Pipeline failed:', err);
    } else {
      console.log('Pipeline succeeded');
    }
  }
);

Ключевые оптимизации:

  1. Streams вместо загрузки в память — обрабатываем данные по частям.
  2. Backpressure управляется автоматически.
  3. Асинхронная обработка не блокирует Event Loop.
  4. Пакетная запись в БД уменьшает количество запросов.

Дополнительные подходы для CPU-heavy задач:

  • Вынос тяжёлых вычислений в Worker Threads.
  • Использование setImmediate() для разбивки долгих синхронных задач.

Ответ 18+ 🔞

А, ну ты глянь, какие задачи у людей бывают! Обработка CSV-файла на сотни мегабайт. Ёпта, классика жанра. Каждый второй думает: «Да чё там, файлик как файлик», а потом получает овердохуища данных в оперативке и сервер накрывается медным тазом.

Вот смотри, как обычно делают новички, у которых доверия ебать ноль к объёмам данных. Берут и грузят весь файл в память разом.

const fs = require('fs');
const csv = require('csv-parser');

// Проблема: весь файл загружается в память
fs.readFile('huge-file.csv', 'utf8', (err, data) => {
  if (err) throw err;
  const lines = data.split('n');
  const results = [];

  lines.forEach(line => {
    // Обработка каждой строки
    const processed = transformLine(line);
    results.push(processed);
  });

  // Массив results может занимать гигабайты памяти
  saveToDatabase(results);
});

Ну и что мы видим? fs.readFile – это пиздопроебибна идея для большого файла. Он тупо вывалит всё содержимое в переменную data. Потом её ещё на строки режут, потом в массив пихают... Да это же хитрая жопа, которая взорвётся при первом же гигабайте! Памяти ебушки-воробушки начнут летать, а приложение просто бздеть и умрёт.

А теперь, чувак, смотри как надо, по-взрослому. Берём потоки (Streams). Это как водопровод: данные текут по трубам, обрабатываются по чуть-чуть и не забивают всю систему.

const fs = require('fs');
const { pipeline } = require('stream');
const csv = require('csv-parser');
const { Transform } = require('stream');

// Кастомный Transform stream для обработки данных
const transformer = new Transform({
  objectMode: true,
  transform(chunk, encoding, callback) {
    // Обработка одной строки CSV
    const processed = {
      id: chunk.id,
      value: calculateValue(chunk),
      timestamp: new Date()
    };
    callback(null, processed);
  }
});

// Пайплайн: чтение → парсинг CSV → трансформация → запись в БД
pipeline(
  fs.createReadStream('huge-file.csv'),
  csv(),
  transformer,
  async function (source) {
    for await (const chunk of source) {
      // Пакетная запись в БД
      await saveToDatabaseBatch(chunk);
    }
  },
  (err) => {
    if (err) {
      console.error('Pipeline failed:', err);
    } else {
      console.log('Pipeline succeeded');
    }
  }
);

Вот это уже дело! Ёперный театр, да? Всё красиво и правильно.

  1. Потоки вместо грубой силы. Файл читается кусочками, парсится по строчкам, и каждая строчка сразу уходит на обработку. В памяти в один момент времени — лишь маленький кусочек данных, а не хуй с горы.
  2. Backpressure. Это когда следующая труба в пайплайне говорит: «Э, подозрение ебать чувствую, я ещё не переварил предыдущий кусок, притормози!» И чтение файла автоматически замедляется. Всё само регулируется, терпения ноль ебать не нужно.
  3. Асинхронность. Event Loop не блокируется долгими операциями, сервер может параллельно другие запросы обрабатывать.
  4. Пакетная запись. Это вообще святое. Не надо дёргать базу данных на каждую строчку — э сабака сука по производительности. Накопили 1000 обработанных записей — одним запросом вкинули. Экономия — ядрёна вошь.

А если там внутри calculateValue(chunk) такая манда с ушами, которая ядро процессора в труху превращает? Тогда уже э бошка думай про Worker Threads. Выноси эти тяжёлые вычисления в отдельный поток, чтобы главный не подвис. Или хотя бы setImmediate() используй, чтобы дать Event Loop'у передохнуть между порциями данных.

Короче, мораль простая: не лезь в пизду с readFile для больших данных. Потоки — твой друг. Иначе будет тебе хиросима в оперативной памяти, а не обработка файла.