Node.js изменил подход к разработке веб-приложений, позволив использовать JavaScript не только на стороне клиента, но и на сервере. Созданный в 2009 году Райаном Далем, этот открытый, кроссплатформенный runtime превратился в основной инструмент современного веб-разработчика. Сегодня трудно представить веб-разработку без Node.js — от стартапов до крупных корпораций вроде Netflix, PayPal или LinkedIn, многие полагаются на его возможности.
Но что же делает Node.js таким особенным и почему он завоевал такую популярность? Секрет кроется в его внутренней архитектуре и неблокирующей модели выполнения кода. В то время как традиционные серверные технологии обрабатывают запросы последовательно, Node.js использует событийно-ориентированный подход, который позволяет обрабатывать многочисленные операции параллельно. Чтобы понять гениальность идеи Райана Даля, нужно вернуться к проблемам, которые существовали в веб-разработке в конце 2000-х. Традиционные серверы, такие как Apache, создавали отдельный поток для каждого подключения. При большом количестве одновременных пользователей это приводило к существенным затратам ресурсов и снижению производительности. Даль заметил, что большую часть времени эти потоки просто "ждали" — ждали завершения операций ввода-вывода, таких как чтение с диска или запрос к базе данных.
Ключевая идея Даля состояла в том, чтобы создать платформу, которая не блокирует выполнение программы во время ожидания завершения операций ввода-вывода. Вместо создания нового потока для каждого запроса, Node.js использует единственный основной поток и событийный цикл. Когда приходит запрос, требующий операции ввода-вывода, Node.js регистрирует функцию обратного вызова (колбэк) и продолжает обрабатывать другие запросы, не блокируя главный поток. Когда операция ввода-вывода завершается, соответствующий колбэк выполняется.
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Пример неблокирующего кода в Node.js
const fs = require('fs');
// Асинхронное чтение файла, не блокирующее основной поток
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
// Этот код выполнится сразу, не дожидаясь чтения файла
console.log('Продолжаем выполнение программы...'); |
|
Такой подход к обработке ввода-вывода известен как "неблокирующий ввод-вывод" или "асинхронный ввод-вывод". В основе Node.js лежит библиотека libuv, которая обеспечивает асинхронный интерфейс к операциям ввода-вывода на разных операционных системах, а также Google V8 — высокопроизводительный движок JavaScript, первоначально разработанный для браузера Chrome.
Выбор однопоточной модели для Node.js был осознанным решением, направленным на решение конкретной проблемы — эффективной обработки многочисленных одновременных соединений с минимальными накладными расходами. Многопоточные модели, используемые в других серверных технологиях, имеют свои преимущества, особенно для вычислительно интенсивных задач, но они также несут с собой сложности, связанные с синхронизацией потоков и разделяемым состоянием. Node.js, напротив, предлагает более простую модель программирования, где разработчик не должен заботиться о блокировках и состоянии гонки (race conditions). Однако эта простота имеет свою цену: выполнение вычислительно интенсивных операций в главном потоке может заблокировать событийный цикл и снизить отзывчивость всего приложения. В последних версиях Node.js эту проблему частично решают с помощью Worker Threads API, который позволяет выполнять JavaScript-код в отдельных потоках.
Компоненты системы
Чтобы понять, как Node.js справляется с асинхронными операциями, нужно разобраться в ключевых компонентах, из которых он состоит. В сердце Node.js находятся движок V8, библиотека libuv, а также система модулей и управления памятью — все эти элементы формируют мощный фундамент для создания высокопроизводительных приложений.
V8 и его роль в интерпретации JavaScript
Движок V8, разработанный Google для браузера Chrome, — первый краеугольный камень архитектуры Node.js. Этот высокопроизводительный интерпретатор JavaScript, написанный на C++, превращает код JavaScript в машинный код, а не просто интерпретирует его. Такой подход существенно ускоряет выполнение программ. V8 использует технологию Just-In-Time (JIT) компиляции, которая анализирует код во время выполнения и оптимизирует часто используемые функции. Вместо просто интерпретации скрипта строка за строкой, V8 компилирует JavaScript напрямую в нативный машинный код перед выполнением, что значительно повышает скорость работы.
Одно из ключевых преимуществ V8 — эффективный сборщик мусора (garbage collector), который автоматически освобождает память, когда объекты становятся недоступными. V8 использует генерационный подход к сборке мусора, разделяя объекты на "молодые" и "старые" поколения, что позволяет оптимизировать процесс очистки памяти.
JavaScript | 1
2
3
4
5
6
7
8
9
10
| // V8 оптимизирует такой код на лету
function sum(a, b) {
return a + b;
}
// При многократном вызове с числами V8 может оптимизировать
// эту функцию специально для работы с числами
for (let i = 0; i < 100000; i++) {
sum(i, i+1);
} |
|
V8 также использует так называемые "скрытые классы" (hidden classes) — внутренние структуры, которые помогают оптимизировать доступ к свойствам объектов. Когда код создаёт много объектов с одинаковой структурой, V8 может существенно ускорить доступ к их свойствам.
Libuv и управление асинхронными операциями
Вторым фундаментальным компонентом Node.js является библиотека libuv — кроссплатформенная C-библиотека, отвечающая за абстрагирование неблокирующих операций ввода-вывода. Именно libuv делает возможным асинхронное выполнение кода в Node.js, независимо от операционной системы.
Libuv предоставляет не только событийный цикл, но и пул потоков для выполнения тяжёлых задач, которые нельзя выполнить асинхронно на уровне операционной системы. Это особенно важно для файловых операций в некоторых операционных системах, где нет нативной поддержки асинхронных файловых операций.
JavaScript | 1
2
3
4
5
6
7
8
| const fs = require('fs');
// Эта операция делегируется libuv, которая использует потоки из пула
// для выполнения чтения файла без блокирования основного потока
fs.readFile('/path/to/file', (err, data) => {
if (err) throw err;
console.log(data);
}); |
|
Когда Node.js выполняет асинхронную операцию, libuv регистрирует соответствующий запрос. В зависимости от типа операции, libuv либо использует асинхронные механизмы операционной системы (например, epoll в Linux, kqueue в macOS или IOCP в Windows), либо делегирует задачу пулу потоков, если асинхронный API недоступен.
Модульная система и управление памятью
Node.js использует модульную систему для организации кода в многократно используемые компоненты. Изначально Node.js опирался на спецификацию CommonJS, которая определяет способ структурирования и загрузки модулей. В более поздних версиях добавилась поддержка ES модулей, ставших стандартом в современном JavaScript. В CommonJS модули загружаются с помощью функции require() , а экспортируются через объект module.exports :
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // math.js
module.exports = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
}
};
// app.js
const math = require('./math');
console.log(math.add(5, 3)); // 8 |
|
С ES модулями синтаксис стал более декларативным, используя ключевые слова import и export :
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
| // math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// app.js
import { add, subtract } from './math';
console.log(add(5, 3)); // 8 |
|
Что касается управления памятью, Node.js полагается на сборщик мусора V8. Однако работа с памятью в серверных приложениях имеет свои особенности. Например, обработка больших объемов данных может приводить к утечкам памяти, если ссылки на объекты сохраняются дольше, чем это необходимо.
Node.js предоставляет инструменты для мониторинга и профилирования использования памяти, что помогает выявлять и устранять проблемы:
JavaScript | 1
2
3
4
| const memoryUsage = process.memoryUsage();
console.log(memoryUsage);
// Выведет что-то вроде:
// { rss: 26247168, heapTotal: 5767168, heapUsed: 3573032, external: 8772 } |
|
Где rss (Resident Set Size) — это объем памяти, выделенный для процесса Node.js, включая сам код, стек и кучу; heapTotal — общий размер кучи JavaScript; heapUsed — используемая часть кучи; external — память, используемая V8 для объектов, привязанных к JavaScript из C++, таких как буферы.
Взаимодействие JavaScript с низкоуровневыми компонентами
Одна из сильных сторон Node.js — возможность использовать JavaScript для управления низкоуровневыми операциями. Это достигается через набор встроенных модулей, таких как fs для работы с файловой системой, net для сетевого программирования и http для создания веб-серверов.
Когда вы вызываете API Node.js из JavaScript, происходит следующее:
1. Ваш JavaScript-код вызывает функцию Node.js API (например, fs.readFile ).
2. Node.js обрабатывает вызов и создает соответствующую задачу для libuv.
3. Libuv планирует выполнение задачи, используя событийный цикл или пул потоков.
4. Когда задача завершается, libuv сигнализирует об этом Node.js.
5. Node.js вызывает соответствующий колбэк в JavaScript.
Этот механизм связывания JavaScript с низкоуровневыми возможностями системы позволяет писать асинхронный код, который звучит легко и понятно для разработчиков. При этом под поверхностью происходит сложная работа по организации неблокирующих операций.
Система модулей: от CommonJS к ES Modules
Хотя базовые принципы модульной системы мы уже затронули, стоит глубже погрузиться в то, как разные системы модулей работают в среде Node.js. CommonJS стал стандартом де-факто для Node.js с самого начала. Его синтаксис уже знаком многим разработчикам:
JavaScript | 1
2
3
4
5
6
7
| // Импорт модуля
const http = require('http');
// Экспорт функциональности
module.exports = function(req, res) {
res.end('Hello World');
}; |
|
Но что происходит, когда вы вызываете require() ? Процесс загрузки модуля включает несколько шагов:
1. Резолвинг пути к модулю (поиск файла по указанному пути или в node_modules ).
2. Загрузка файла модуля и его обертывание в функцию.
3. Компиляция и выполнение кода модуля.
4. Кэширование результатов для дальнейшего использования.
CommonJS загружает модули синхронно — каждый вызов require() блокирует выполнение, пока модуль не будет загружен полностью. Это не проблема для серверных приложений, но становится неприемлемым в браузерной среде, что послужило толчком к разработке ES Modules.
ES Modules, появившиеся в спецификации ECMAScript 2015 (ES6), предлагают статический импорт, что позволяет анализировать зависимости во время компиляции, а не во время выполнения:
JavaScript | 1
2
3
4
5
6
7
8
9
10
| // Импорт с деструктуризацией
import { createServer } from 'http';
// Импорт по умолчанию
import express from 'express';
// Экспорт
export function handler(req, res) {
res.end('Hello from ES modules');
} |
|
Node.js постепенно внедрял поддержку ES Modules. Начиная с версии 13.2.0, ES Modules перестали считаться экспериментальной функциональностью. Сегодня можно использовать файлы с расширением .mjs или указать "type": "module" в файле package.json для использования ES Modules в проекте.
Хотя ES Modules представляют более современный и гибкий подход, переход всей экосистемы Node.js с CommonJS занимает время, и многие пакеты до сих пор используют CommonJS. К счастью, Node.js обеспечивает совместимость между системами модулей, позволяя импортировать CommonJS модули в ES Modules и наоборот, с некоторыми ограничениями.
Работа сборщика мусора V8 в контексте Node.js
Сборщик мусора (GC) V8 играет критическую роль в управлении памятью приложений Node.js. Понимание его работы помогает писать более производительный код и избегать утечек памяти. V8 использует генерационный подход к сборке мусора, разделяя кучу на несколько поколений:
1. Молодое поколение (Nursery) — новые объекты создаются здесь,
2. Промежуточное поколение (Intermediate) — объекты, пережившие несколько сборок,
3. Старое поколение (Old) — долгоживущие объекты.
Большинство объектов в JavaScript-приложениях живут недолго. Это называется "гипотезой недолговечности" (Generational Hypothesis). Основываясь на этой гипотезе, V8 оптимизирует сборку мусора, фокусируясь прежде всего на молодом поколении, где сборка происходит чаще и быстрее.
JavaScript | 1
2
3
4
5
6
| // Этот код создаёт много временных объектов, которые быстро становятся мусором
function processLargeArray(arr) {
return arr.map(x => x * 2)
.filter(x => x > 10)
.reduce((acc, x) => acc + x, 0);
} |
|
В Node.js-приложениях, особенно серверах с высокой нагрузкой, важно понимать, как GC влияет на производительность. Когда происходит полная сборка мусора (Major GC), она может приостановить выполнение JavaScript, что потенциально приводит к задержкам в обработке запросов. В современных версиях V8 используются различные оптимизации для минимизации задержек:- Параллельная маркировка (Concurrent marking) — маркировка объектов происходит параллельно с выполнением JavaScript.
- Инкрементальная сборка мусора — процесс разбивается на мелкие шаги для сокращения пауз.
- Компактификация — перераспределение памяти для уменьшения фрагментации.
Node.js предоставляет флаги командной строки для тонкой настройки GC, что может быть полезно для приложений с особыми требованиями к производительности:
Bash | 1
2
| # Пример запуска Node.js с настройками GC
node --max-old-space-size=4096 --optimize-for-size app.js |
|
Нативные аддоны и их интеграция с ядром Node.js
Несмотря на мощь JavaScript, иногда необходимо интегрироваться с нативным кодом для достижения максимальной производительности или доступа к низкоуровневым API. Для этого в Node.js существуют нативные аддоны — динамически подключаемые библиотеки, написанные на C/C++. Нативные аддоны могут выполнять несколько важных функций:- Обеспечивать высокопроизводительные операции, требующие прямого доступа к CPU/GPU.
- Предоставлять мост к существующим C/C++ библиотекам.
- Реализовывать функции, требующие прямого доступа к системным ресурсам.
Классический способ создания нативных аддонов — использование Node.js API (N-API), который предоставляет стабильный ABI (Application Binary Interface):
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // hello.cc
#include <node.h>
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(
isolate, "world").ToLocalChecked());
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Method);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo |
|
Создание и компиляция нативных аддонов требует установки компилятора и знания системы сборки node-gyp. Этот процесс гораздо сложнее, чем написание чистого JavaScript, но результаты могут стоить затраченных усилий для критичных к производительности частей приложения. Благодаря N-API, Node.js гарантирует, что нативные аддоны будут работать с разными версиями Node без необходимости перекомпиляции, что раньше было серьезной проблемой.
Работа с бинарными данными и типизированными массивами
Обработка бинарных данных — неотъемлемая часть многих серверных приложений, от потоковой передачи файлов до взаимодействия с базами данных и внешними API. Node.js предоставляет несколько мощных примитивов для работы с бинарными данными. Главный из них — это класс Buffer , который представляет собой последовательность байтов фиксированной длины. Буферы похожи на массивы целых чисел, но соответствуют блокам памяти за пределами стандартной JavaScript-кучи:
JavaScript | 1
2
3
4
5
6
7
| // Создание буфера
const buf = Buffer.from('Hello World', 'utf8');
console.log(buf.toString('hex')); // 48656c6c6f20576f726c64
// Работа с бинарными данными
const binaryData = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]);
console.log(binaryData.toString()); // "buffer" |
|
Буферы особенно полезны при работе с файлами и сетью. Например, при чтении файла можно указать, что нужно получить результат в виде буфера, а не строки:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
| const fs = require('fs');
fs.readFile('image.png', (err, buffer) => {
if (err) throw err;
// buffer содержит бинарные данные изображения
console.log(`Размер файла: ${buffer.length} байт`);
// Можно манипулировать битами напрямую
const magicNumber = buffer.slice(0, 4).toString('hex');
console.log(`Магические байты: ${magicNumber}`);
}); |
|
Помимо буферов, Node.js поддерживает стандартные JavaScript типизированные массивы, такие как Uint8Array и Float64Array , которые предоставляют структурированный доступ к бинарным данным:
JavaScript | 1
2
3
4
5
6
7
8
9
| // Создание типизированного массива из буфера
const buf = Buffer.from([1, 2, 3, 4]);
const uint32array = new Uint32Array(buf.buffer, buf.byteOffset, buf.length / Uint32Array.BYTES_PER_ELEMENT);
// Можно использовать типизированные массивы напрямую
const float64Array = new Float64Array(10);
for (let i = 0; i < float64Array.length; i++) {
float64Array[i] = Math.random();
} |
|
Интересно, что Buffer в Node.js фактически наследуется от Uint8Array , что обеспечивает совместимость и позволяет использовать все методы типизированных массивов с буферами.
Для обработки крупных массивов бинарных данных без загрузки их целиком в память, Node.js предоставляет мощную абстракцию — потоки (Streams). Они позволяют обрабатывать данные по мере их поступления, что существенно уменьшает потребление памяти:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const fs = require('fs');
const crypto = require('crypto');
// Вычисление хеша большого файла без загрузки его целиком в память
const hash = crypto.createHash('sha256');
const input = fs.createReadStream('largefile.bin');
input.on('data', chunk => {
hash.update(chunk);
});
input.on('end', () => {
console.log(hash.digest('hex'));
}); |
|
Понимание этих компонентов системы Node.js — ключ к написанию эффективных, надежных и безопасных приложений. В следующем разделе мы детально рассмотрим, как функционирует событийный цикл — механизм, который связывает все эти компоненты воедино и обеспечивает асинхронную природу Node.js.
Не могу с решениями задач на node js (я понимаю как их решить на js, но как на node js не знаю) 1) Однажды ковбой Джо решил обзавестись револьвером и пришёл в оружейный магазин. У ковбоя s долларов, а на выбор представлены n револьверов с... Uncaught TypeError: Failed to execute 'removeChild' on 'Node': parameter 1 is not of type 'Node' Привет, есть следующий код который срабатывает правильно, как и задумано (когда создано 10параграфов - удает все), но выдает ошибку в консоль... Не запускается пакет node js - пакетами? npm? сам node? gulp? Всем доброго времени суток.
Есть такая проблема, пытаюсь перебраться на Linux (Ubuntu) Установил node js по докам (да и вообще как только не... Выложил приложение Node js на хост, ошибка (node:12900) [DEP0005] DeprecationWarning: Buffer() Выложил приложение Node js на хост, ошибка (node:12900) DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use...
Событийный цикл подробно
Событийный цикл — сердце асинхронной модели Node.js. Это механизм, который позволяет однопоточному JavaScript обрабатывать тысячи одновременных соединений без блокировки выполнения. Многие разработчики, даже те, кто годами работает с Node.js, не до конца понимают, как функционирует событийный цикл. Давайте погрузимся в его устройство.
Фазы цикла и их назначение
Событийный цикл Node.js не просто бесконечный цикл — он имеет чётко определённую структуру, состоящую из нескольких фаз, выполняемых последовательно:
1. Фаза таймеров (Timers): Выполняет колбэки, запланированные через setTimeout() и setInterval() . Важно понимать, что указанное в этих функциях время — это минимальная задержка, а не гарантированный момент выполнения.
2. Фаза отложенных колбэков (Pending Callbacks): Выполняет колбэки операций ввода-вывода, отложенных до следующей итерации цикла. Типичный пример — некоторые системные операции, такие как TCP ошибки.
3. Фаза подготовки (Idle, Prepare): Используется внутренне Node.js и редко имеет значение для прикладных разработчиков.
4. Фаза опроса (Poll): Ключевая фаза цикла. Здесь происходит:
- Вычисление времени блокировки для ввода-вывода.
- Обработка событий из очереди опроса.
- Выполнение колбэков по мере их появления.
Если очередь пуста, Node.js может ждать здесь новых событий (если нет запланированных таймеров или immediate).
5. Фаза проверки (Check): Выполняет колбэки, зарегистрированные через setImmediate() . Эта фаза выполняется сразу после фазы опроса.
6. Фаза закрытия (Close Callbacks): Выполняет колбэки закрытия, например socket.on('close', ...) .
После завершения всех фаз цикл начинается заново. Кроме того, между любыми фазами выполняются микрозадачи (process.nextTick() и промисы).
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Пример, демонстрирующий порядок выполнения фаз
console.log('Начало программы');
setTimeout(() => {
console.log('Таймер');
}, 0);
setImmediate(() => {
console.log('Immediate');
});
process.nextTick(() => {
console.log('NextTick');
});
console.log('Конец программы');
// Вывод:
// Начало программы
// Конец программы
// NextTick
// Таймер (или Immediate - может варьироваться)
// Immediate (или Таймер - может варьироваться) |
|
Порядок выполнения setTimeout(fn, 0) и setImmediate() может различаться в зависимости от контекста, загруженности системы и других факторов. Однако, внутри колбэков ввода-вывода setImmediate() всегда выполнится раньше таймеров:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const fs = require('fs');
fs.readFile('some-file.txt', () => {
setTimeout(() => {
console.log('Таймер внутри I/O колбэка');
}, 0);
setImmediate(() => {
console.log('Immediate внутри I/O колбэка');
});
});
// Вывод всегда будет:
// Immediate внутри I/O колбэка
// Таймер внутри I/O колбэка |
|
Микрозадачи и макрозадачи в событийном цикле
В контексте событийного цикла задачи делятся на два типа:
Макрозадачи (Macrotasks) — это обычные асинхронные операции:setTimeout /setInterval ,
setImmediate ,
- Операции ввода-вывода,
- Обработчики событий.
Микрозадачи (Microtasks) выполняются сразу после текущей операции, до следующей макрозадачи:process.nextTick() — специфичный для Node.js метод, который имеет наивысший приоритет среди всех асинхронных операций.
- Обработчики промисов (
.then() , .catch() , .finally() ).
queueMicrotask() .
Вот ключевое различие: микрозадачи выполняются сразу после текущей операции и до начала следующей фазы цикла, в то время как макрозадачи выполняются в соответствующих фазах событийного цикла.
JavaScript | 1
2
3
4
5
6
| Promise.resolve().then(() => console.log('Промис'));
process.nextTick(() => console.log('nextTick'));
// Вывод всегда:
// nextTick
// Промис |
|
Это поведение критически важно для понимания потока выполнения асинхронного кода.
Практический анализ очередности выполнения колбэков
Чтобы лучше понять очерёдность выполнения разных типов задач, рассмотрим сложный пример:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| console.log('1. Синхронный код');
setTimeout(() => {
console.log('2. Таймер 1');
process.nextTick(() => {
console.log('3. nextTick внутри таймера');
});
Promise.resolve().then(() => {
console.log('4. Промис внутри таймера');
});
}, 0);
process.nextTick(() => {
console.log('5. nextTick 1');
setTimeout(() => {
console.log('6. Вложенный таймер в nextTick');
}, 0);
});
Promise.resolve().then(() => {
console.log('7. Промис 1');
process.nextTick(() => {
console.log('8. nextTick внутри промиса');
});
});
setTimeout(() => {
console.log('9. Таймер 2');
}, 0);
setImmediate(() => {
console.log('10. Immediate');
});
console.log('11. Ещё синхронный код'); |
|
Результат выполнения:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
| 1. Синхронный код
11. Ещё синхронный код
5. nextTick 1
7. Промис 1
8. nextTick внутри промиса
2. Таймер 1
3. nextTick внутри таймера
4. Промис внутри таймера
9. Таймер 2
10. Immediate
6. Вложенный таймер в nextTick |
|
Анализ очередности:
1. Сначала выполняется весь синхронный код.
2. Затем обрабатываются все микрозадачи из очереди (nextTick , промисы).
3. Далее идет выполнение фаз событийного цикла (таймеры, ввод-вывод, immediates).
4. После выполнения каждой макрозадачи снова проверяется и очищается очередь микрозадач.
Важно отметить несколько моментов:process.nextTick() имеет приоритет над обработчиками промисов.
- Внутри каждой фазы колбэки выполняются в порядке FIFO (First-In-First-Out).
- После каждого выполненного колбэка событийный цикл проверяет очереди микрозадач.
- Между разными макрозадачами (setTimeout и setImmediate) порядок может варьироваться, если они запланированы из синхронного кода.
Понимание этих тонкостей помогает избежать трудноуловимых ошибок, которые часто возникают в асинхронном коде. Как ни странно, но именно это тонкое поведение событийного цикла делает Node.js таким мощным инструментом для создания масштабируемых приложений.
Оптимизация таймеров и работы с setTimeout/setImmediate
Таймеры — одна из самых часто используемых возможностей Node.js, но они не так просты, как может показаться. Неэффективное использование таймеров может привести к ненужным нагрузкам на событийный цикл и деградации производительности. Одна из ключевых вещей, которую нужно понимать о setTimeout и setInterval : указанное время задержки — это минимальное время до выполнения колбэка, но не гарантированное. Если событийный цикл загружен, колбэк может выполниться значительно позже.
JavaScript | 1
2
3
4
5
6
7
| // Создание множества таймеров может вызвать проблемы
for (let i = 0; i < 10000; i++) {
setTimeout(() => {
// Тяжелая операция
const result = Array(10000).fill(1).map(x => x * 2).reduce((a, b) => a + b);
}, 1000);
} |
|
Такой код создаст 10000 таймеров, все они сработают примерно в одно время, что вызовет перегрузку процессора и, возможно, временную блокировку событийного цикла. Вместо этого лучше использовать одиночный таймер с обработкой пакета задач:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| const tasks = Array(10000).fill(null).map((_, i) => i);
let completed = 0;
setTimeout(() => {
// Обрабатываем задачи пакетами
const processBatch = () => {
const batch = tasks.slice(completed, completed + 500);
if (batch.length === 0) return;
batch.forEach(taskId => {
// Выполнение задачи
const result = Array(10000).fill(1).map(x => x * 2).reduce((a, b) => a + b);
});
completed += batch.length;
// Отдаём управление событийному циклу, используя setImmediate
setImmediate(processBatch);
};
processBatch();
}, 1000); |
|
Использование setImmediate вместо вложенных setTimeout может существенно улучшить производительность для операций, которые должны выполняться как можно скорее, но не блокировать поток выполнения. В отличие от setTimeout(fn, 0) , setImmediate гарантирует выполнение в следующей итерации цикла, как только завершится фаза опроса (poll).
Обработка исключений в асинхронном контексте
Одна из особенностей асинхронного программирования в Node.js — сложность отлова исключений. В синхронном коде мы привыкли использовать конструкцию try-catch :
JavaScript | 1
2
3
4
5
6
| try {
const result = riskyOperation();
// Обработка результата
} catch (error) {
console.error('Ошибка перехвачена:', error);
} |
|
Однако в асинхронном коде этот подход часто не срабатывает:
JavaScript | 1
2
3
4
5
6
7
8
| try {
setTimeout(() => {
throw new Error('Асинхронная ошибка');
}, 100);
} catch (error) {
// Этот блок никогда не выполнится
console.error('Ошибка перехвачена?', error);
} |
|
Когда ошибка возникает в асинхронной функции, стек вызовов, который существовал при планировании операции, уже не существует, когда операция фактически выполняется. Поэтому здесь нужны другие механизмы:
1. Колбэки с проверкой ошибок — традиционный подход в Node.js:
JavaScript | 1
2
3
4
5
6
7
| fs.readFile('file.txt', (err, data) => {
if (err) {
console.error('Ошибка чтения файла:', err);
return;
}
// Обработка данных
}); |
|
2. Промисы с .catch() :
JavaScript | 1
2
3
4
5
6
7
| fs.promises.readFile('file.txt')
.then(data => {
// Обработка данных
})
.catch(err => {
console.error('Ошибка чтения файла:', err);
}); |
|
3. Async/await с try-catch:
JavaScript | 1
2
3
4
5
6
7
8
| async function readFileContent() {
try {
const data = await fs.promises.readFile('file.txt');
// Обработка данных
} catch (err) {
console.error('Ошибка чтения файла:', err);
}
} |
|
4. Обработка необработанных отклонений промисов:
JavaScript | 1
2
3
4
5
| process.on('unhandledRejection', (reason, promise) => {
console.error('Необработанное отклонение промиса:', reason);
// Можно завершить процесс или выполнить другие действия
// process.exit(1);
}); |
|
5. Обработка необработанных исключений:
JavaScript | 1
2
3
4
5
6
| process.on('uncaughtException', (err) => {
console.error('Необработанное исключение:', err);
// Важно: после необработанного исключения состояние процесса может быть нестабильным
// Рекомендуется выполнить логирование и перезапустить процесс
process.exit(1);
}); |
|
Стратегии предотвращения блокировки событийного цикла
Блокировка событийного цикла происходит, когда долго выполняющаяся синхронная операция не даёт Node.js обрабатывать другие задачи. Это может привести к существенному снижению производительности или полной неотзывчивости сервера.
Основные признаки блокировки:- Увеличение времени отклика сервера.
- Сообщения "Event loop blocked for X ms" в логах мониторинга.
- Нестабильная производительность под нагрузкой.
Рассмотрим несколько стратегий для предотвращения блокировки:
1. Разбиение тяжёлых вычислений на части:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| function processLargeArray(array, batchSize = 1000) {
let index = 0;
function processBatch() {
const end = Math.min(index + batchSize, array.length);
// Обработка текущего пакета
for (let i = index; i < end; i++) {
// Тяжелая операция над элементом
array[i] = heavyComputationOnItem(array[i]);
}
index = end;
// Если есть еще элементы, планируем следующий пакет
if (index < array.length) {
setImmediate(processBatch);
} else {
console.log('Обработка завершена');
}
}
// Начинаем обработку
processBatch();
} |
|
2. Использование таймеров для разделения вычислений:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| function calculatePrimes(max) {
const primes = [];
let currentNumber = 2;
function checkNextBatch() {
const endTime = Date.now() + 50; // Максимум 50мс на пакет
while (Date.now() < endTime && currentNumber <= max) {
let isPrime = true;
for (let i = 2; i <= Math.sqrt(currentNumber); i++) {
if (currentNumber % i === 0) {
isPrime = false;
break;
}
}
if (isPrime) primes.push(currentNumber);
currentNumber++;
}
if (currentNumber <= max) {
// Еще не закончили, планируем следующий пакет
setTimeout(checkNextBatch, 0);
} else {
console.log(`Найдено ${primes.length} простых чисел`);
}
}
checkNextBatch();
} |
|
3. Отслеживание задержек событийного цикла:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| let lastCheck = Date.now();
setInterval(() => {
const now = Date.now();
const delay = now - lastCheck - 1000; // Ожидаемая задержка 1000мс
if (delay > 100) { // Задержка более 100мс считается проблемной
console.warn(`Событийный цикл был заблокирован на ${delay}мс`);
// Можно отправить метрику в систему мониторинга
}
lastCheck = now;
}, 1000); |
|
Работа с многопоточностью через Worker Threads API
Несмотря на однопоточную природу основного процесса Node.js, с версии 10.5.0 доступен Worker Threads API, который позволяет запускать JavaScript в отдельных потоках. Это особенно полезно для ресурсоёмких вычислений, которые могут блокировать основной поток. Создание и использование работника (worker):
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
// Это основной поток
const worker = new Worker(__filename, {
workerData: { input: Array(1000000).fill(1) }
});
worker.on('message', result => {
console.log('Результат от работника:', result);
});
worker.on('error', err => {
console.error('Ошибка работника:', err);
});
worker.on('exit', code => {
if (code !== 0)
console.error(`Работник завершился с кодом ${code}`);
});
} else {
// Это работник
const { input } = workerData;
// Выполняем тяжелую операцию без блокировки основного потока
const result = input.map(x => x * 2).reduce((a, b) => a + b, 0);
// Отправляем результат основному потоку
parentPort.postMessage(result);
} |
|
Worker Threads предлагают несколько преимуществ перед другими формами параллелизма в Node.js, такими как child_process:
- Они используют меньше ресурсов, чем отдельные процессы.
- Они могут делить память с помощью
SharedArrayBuffer .
- Они могут обмениваться данными через
MessagePort .
Пример использования SharedArrayBuffer :
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| const { Worker, isMainThread } = require('worker_threads');
if (isMainThread) {
// Создаем SharedArrayBuffer, доступный между потоками
const buffer = new SharedArrayBuffer(4);
const view = new Int32Array(buffer);
view[0] = 0;
const worker = new Worker(__filename, { workerData: { buffer } });
// Запускаем таймер для чтения обновлений
setInterval(() => {
console.log('Текущее значение:', Atomics.load(view, 0));
}, 100);
} else {
const { buffer } = workerData;
const view = new Int32Array(buffer);
// Инкрементируем значение каждые 500мс
setInterval(() => {
Atomics.add(view, 0, 1);
}, 500);
} |
|
Worker Threads особенно полезны для:- Сложных математических вычислений.
- Обработки больших массивов данных.
- Работы с криптографией.
- Обработки изображений и видео.
- Других CPU-интенсивных задач.
Понимание событийного цикла и его оптимизация — ключевые навыки для разработчика Node.js. Эффективное использование таймеров, правильная обработка исключений, предотвращение блокировок и применение многопоточности в нужных местах позволят создавать высокопроизводительные, отказоустойчивые приложения, способные обрабатывать тысячи запросов одновременно.
Производительность на практике: особенности тонкой настройки
В практической разработке Node.js приложений производительность - один из ключевых факторов успеха. Нередко разработчики сталкиваются с неочевидными узкими местами, которые могут существенно снизить быстродействие системы. В этом разделе разберём типичные проблемы производительности и методы их решения. Профилирование Node.js приложений - первый шаг к выявлению проблем производительности. Встроенные возможности Node.js позволяют собирать детальную информацию о работе приложения:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Профилирование CPU
const profiler = require('v8-profiler-next');
const fs = require('fs');
// Начинаем профилирование
profiler.startProfiling('CPU Profile');
// Через некоторое время останавливаем и сохраняем результат
setTimeout(() => {
const profile = profiler.stopProfiling();
profile.export()
.pipe(fs.createWriteStream('./cpuprofile.cpuprofile'))
.on('finish', () => profile.delete());
}, 30000); |
|
Утечки памяти - ещё одна распространённая проблема. Они могут возникать из-за замыканий, неосвобождённых обработчиков событий или неправильной работы с кешем. Простой пример утечки памяти:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
| const cache = new Map();
function badCaching(key, value) {
// Отсутствует механизм очистки кеша
cache.set(key, value);
// Кеш будет расти бесконечно
setInterval(() => {
console.log(cache.get(key));
}, 1000);
} |
|
Исправленная версия с механизмом очистки:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| class LRUCache {
constructor(maxSize = 1000) {
this.cache = new Map();
this.maxSize = maxSize;
}
set(key, value) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
get(key) {
const value = this.cache.get(key);
if (value) {
// Обновляем позицию элемента
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
} |
|
Параллельная обработка данных может значительно улучшить производительность. Worker Threads API предоставляет удобный способ распределения нагрузки:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const os = require('os');
function processDataParallel(data, processingFn) {
return new Promise((resolve, reject) => {
const numCPUs = os.cpus().length;
const chunkSize = Math.ceil(data.length / numCPUs);
const workers = [];
let completedWorkers = 0;
const results = [];
for (let i = 0; i < numCPUs; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, data.length);
const worker = new Worker(`
const { parentPort, workerData } = require('worker_threads');
const { data, start, end, fnStr } = workerData;
const processingFn = eval('(' + fnStr + ')');
const result = data.slice(start, end).map(processingFn);
parentPort.postMessage(result);
`, {
eval: true,
workerData: {
data,
start,
end,
fnStr: processingFn.toString()
}
});
worker.on('message', (result) => {
results[i] = result;
completedWorkers++;
if (completedWorkers === numCPUs) {
resolve([].concat(...results));
}
});
worker.on('error', reject);
workers.push(worker);
}
});
} |
|
Оптимизация работы с базами данных играет критическую роль в производительности веб-приложений. Пулы соединений и правильная индексация могут значительно ускорить работу:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| const { Pool } = require('pg');
const pool = new Pool({
user: 'dbuser',
host: 'database.server.com',
database: 'mydb',
password: 'secretpassword',
port: 5432,
// Оптимальные настройки пула
max: 20, // максимум соединений
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Переиспользование соединений
async function executeQueries(queries) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const results = [];
for (const query of queries) {
const result = await client.query(query);
results.push(result);
}
await client.query('COMMIT');
return results;
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
} |
|
Кэширование и мемоизация - мощные инструменты оптимизации. Реализация мемоизации с ограничением по времени:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| function memoizeWithTTL(fn, ttl = 60000) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value;
}
const result = fn.apply(this, args);
cache.set(key, {
value: result,
timestamp: Date.now()
});
return result;
};
}
// Пример использования
const expensiveOperation = memoizeWithTTL((n) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(n * n);
}, 1000);
});
}, 5000); // кеш на 5 секунд |
|
Работа с потоками данных (streams) позволяет эффективно обрабатывать большие объёмы данных без загрузки их целиком в память. Пример трансформации CSV-файла:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| const fs = require('fs');
const { Transform } = require('stream');
const csv = require('csv-parser');
const transformStream = new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
// Преобразование данных
const transformed = {
name: chunk.name.toUpperCase(),
age: parseInt(chunk.age) + 1,
city: chunk.city
};
callback(null, JSON.stringify(transformed) + '\n');
}
});
fs.createReadStream('input.csv')
.pipe(csv())
.pipe(transformStream)
.pipe(fs.createWriteStream('output.json'))
.on('finish', () => console.log('Преобразование завершено')); |
|
В целом, оптимизация производительности Node.js приложений требует комплексного подхода и глубокого понимания внутренних механизмов работы платформы. Важно помнить, что преждевременная оптимизация может усложнить код без существенного выигрыша в производительности. Следует начинать с профилирования и выявления реальных узких мест, а затем применять соответствующие методы оптимизации.
Применение Node.js в разных типах приложений
Практический опыт применения Node.js в различных сценариях позволяет выделить области, где эта платформа проявляет себя особенно хорошо, а также ситуации, когда стоит рассмотреть альтернативные решения., Ажно понимать, что Node.js - не универсальный инструмент, и его выбор должен быть обоснован конкретными требованиями проекта.
Сильные стороны Node.js
REST API и микросервисы - естественная среда для Node.js. Возможность быстро обрабатывать множество параллельных запросов и встроенная поддержка JSON делают платформу идеальной для создания API:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| const express = require('express');
const app = express();
class UserService {
constructor() {
this.users = new Map();
}
async createUser(userData) {
const userId = Math.random().toString(36).substr(2, 9);
this.users.set(userId, userData);
return { id: userId, ...userData };
}
async getUser(userId) {
const user = this.users.get(userId);
if (!user) throw new Error('User not found');
return user;
}
}
const userService = new UserService();
app.use(express.json());
app.post('/users', async (req, res) => {
try {
const user = await userService.createUser(req.body);
res.status(201).json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.get('/users/:id', async (req, res) => {
try {
const user = await userService.getUser(req.params.id);
res.json(user);
} catch (err) {
res.status(404).json({ error: err.message });
}
});
app.listen(3000); |
|
Потоковая обработка данных также является сильной стороной Node.js. Встроенная поддержка потоков позволяет обрабатывать большие объёмы данных с минимальным потреблением памяти:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| const { Transform } = require('stream');
const fs = require('fs');
class DataTransformer extends Transform {
constructor(options = {}) {
super({ ...options, objectMode: true });
this.batchSize = options.batchSize || 1000;
this.batch = [];
}
_transform(chunk, encoding, callback) {
this.batch.push(chunk);
if (this.batch.length >= this.batchSize) {
this.processBatch();
}
callback();
}
_flush(callback) {
if (this.batch.length > 0) {
this.processBatch();
}
callback();
}
processBatch() {
// Обработка пакета данных
const result = this.batch.map(item => ({
...item,
processed: true,
timestamp: Date.now()
}));
this.push(result);
this.batch = [];
}
}
// Использование трансформера
fs.createReadStream('input.json')
.pipe(new DataTransformer({ batchSize: 100 }))
.pipe(fs.createWriteStream('output.json')); |
|
Ограничения и компромисы
CPU-интенсивные операции - не самая сильная сторона Node.js. Для таких задач лучше использовать языки, оптимизированные для вычислений, такие как Rust или Go. Однако существуют способы обойти это ограничение:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| const { Worker } = require('worker_threads');
const os = require('os');
class ComputationPool {
constructor(workerScript, numWorkers = os.cpus().length) {
this.workers = new Array(numWorkers).fill(null).map(() =>
new Worker(workerScript)
);
this.nextWorker = 0;
}
async compute(data) {
const worker = this.workers[this.nextWorker];
this.nextWorker = (this.nextWorker + 1) % this.workers.length;
return new Promise((resolve, reject) => {
worker.once('message', resolve);
worker.once('error', reject);
worker.postMessage(data);
});
}
terminate() {
return Promise.all(
this.workers.map(worker => worker.terminate())
);
}
}
// Использование пула для тяжёлых вычислений
const pool = new ComputationPool('./compute-worker.js');
const results = await Promise.all(
tasks.map(task => pool.compute(task))
); |
|
Работа с базами данных требует особого внимания в Node.js. Асинхронная природа платформы может создавать неожиданные проблемы при управлении транзакциями:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| class TransactionManager {
constructor(pool) {
this.pool = pool;
}
async withTransaction(callback) {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
}
// Использование менеджера транзакций
const tm = new TransactionManager(pool);
await tm.withTransaction(async (client) => {
const { rows } = await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2 RETURNING *',
[amount, fromId]
);
if (rows[0].balance < 0) {
throw new Error('Insufficient funds');
}
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId]
);
}); |
|
Масштабирование Node.js приложений имеет свои особенности. PM2 или Docker упрощают управление несколькими экземплярами приложения:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // ecosystem.config.js для PM2
module.exports = {
apps: [{
name: 'api-server',
script: './server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 80
}
}]
}; |
|
Выбор Node.js для проекта должен основываться на тщательном анализе требований. При правильном применении и понимании ограничений платформы, Node.js может стать надёжным фундаментом для широкого спектра приложений - от небольших утилит до крупных распределённых систем.
Async await изнутри - как устроено? const sleep = sec => new Promise(resolve => setTimeout(resolve, sec));
(async() => {
setTimeout(() => console.log('1000'), 1000);
... JQuery изнутри вечер добрый, кто подскажет не просвещенному, один момент из работы jQuery?
var div = $('div');
console.log(div) // result
результат... Почему изнутри функции я не имею доступа к переменной? You can find HTML at smtpromocodes.com
let left = document.getElementById("button1");
let right = document.getElementById("button3");
... Как работает Node.js Всем привет. Вопрос простой - принцып работы node. Я так понимаю - если JS интерпритируемый язык - то значит обходится без компиляции. Node получает... Не работает чат node.js <!doctype html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }... Node не работает возврат из функции var titles = GetTitles();
///....
function GetTitles() {
connection.query('SELECT * FROM titles', function (err, rows) {
return... Не работает css на сервере node.js Создал болванку сайта (связка html + css) и на локальном сервер все работает отлично, а вот когда загружаю в node
var express =... Node v7.9 async/await не работает. Что не так? Всем привет.
Использую node 7.9 и express. Вот код как пример:
function mysql_execute(sql, props) {
return new Promise(function... Какова ситуация с import/export в node.js ? (у меня установлена 7.10, но программа в в WS работает) //режим ES6 включен, webstorm import/export не подчёркивает, версия node 7.10
//в сборках видел применение import/export , но там был babel. Но... Node js не работает на хостинге Всем доброго времени суток. Возникла проблема с запуском node js серверной части на удаленном хостинге. Итак. Вчера оформил машину на... Не работает роутинг Node.js Помогите, пожалуйста. Пишу простое приложение на Node.js + Angular 5. В server.ts пишу
app.get('/login', (req, res) => {
... Не работает node-inspector Добрый день. Почему-то не получается протестировать отладчик node-inspector. Может он тоже устарел и есть модуль посвежее? Поставил npm i -g...
|