Node.js — среда выполнения JavaScript вне браузера, построенная на V8 и расширенная API для сети, файлов, процессов, потоков и ОС. Его используют для REST API, GraphQL, WebSocket-серверов, микросервисов, CLI, proxy и API Gateway. Сильная сторона — неблокирующий I/O: один процесс эффективно обслуживает много соединений. Компромисс в том, что длительные CPU-heavy вычисления могут заблокировать event loop, если не вынести их в workers, processes или отдельный сервис.
JavaScript выполняется в V8, а Node.js API передают асинхронную работу операционной системе или thread pool libuv. Главный JavaScript-поток не ждет завершения I/O и продолжает выполнять другой код. Когда операция завершается, ее callback или Promise continuation становится доступным event loop. Поэтому Node.js достигает высокой конкурентности, хотя пользовательский JavaScript обычно выполняется в одном главном потоке.
V8 компилирует и выполняет JavaScript, управляет объектами и памятью. libuv предоставляет кроссплатформенный event loop, интеграцию async I/O и worker thread pool для части filesystem, DNS и crypto операций. Event loop решает, когда запускать callbacks завершенных операций. Node.js связывает JavaScript API с native capabilities; сам V8 не дает HTTP-сервер, filesystem или process API.
Один Node.js-процесс обычно выполняет пользовательский JavaScript в одном главном потоке, поэтому два обычных request handler не могут выполнять JavaScript одновременно в одном и том же потоке. Но платформа не буквально однопоточная: есть thread pool libuv, async I/O ОС, garbage collection V8, worker_threads и child processes. Фраза относится прежде всего к default модели исполнения JavaScript.
5Как Node.js обрабатывает много запросов одновременно?
Node.js запускает I/O операции и возвращается в event loop вместо того, чтобы блокировать поток на каждый запрос. Например, 1000 запросов к базе могут быть in flight, пока главный поток принимает соединения и обрабатывает готовые callbacks. Это хорошо для I/O-bound workloads: HTTP, база, файлы, sockets. Но тяжелый синхронный CPU-код в handler блокирует event loop и задерживает все остальные запросы.
Node.js использует фазы планирования libuv: pending callbacks, idle/prepare, poll, check и close callbacks, а также обработку таймеров. В poll обрабатываются многие функции обратного вызова ввода-вывода, в check — setImmediate, а таймеры запускают setTimeout/setInterval после достижения порога задержки, когда планирование допускает выполнение. В libuv 1.45 и Node.js 20+ таймеры обрабатываются после poll в каждой итерации, с первоначальным проходом перед входом в цикл ради совместимости. process.nextTick — отдельная очередь Node, а реакции Promise и queueMicrotask используют очередь микрозадач V8; ни одна из них не является фазой libuv. Эти очереди обрабатываются в контрольных точках между функциями обратного вызова. Рекурсивное пополнение очередей может лишать другие задачи возможности выполняться.
7Чем различаются синхронные, асинхронные и неблокирующие операции?
Синхронность описывает передачу управления: вызов возвращает результат или выбрасывает исключение до продолжения вызывающего кода. Асинхронность означает получение результата позже через функцию обратного вызова, Promise или другое уведомление. Блокирование — ожидание работы или ресурса, при котором текущий поток не может выполнять другую работу; неблокирующие API избегают такого ожидания, например возвращая признак необходимости ожидания либо организуя уведомление. Это разные характеристики: синхронность сама по себе не означает ожидания готовности ввода-вывода, а возврат Promise не делает тяжёлые синхронные вычисления неблокирующими. В Node.js fs.readFileSync — блокирующий синхронный API; fs.promises.readFile выдаёт результат асинхронно, пока основной поток может выполнять другую работу. При обработке запросов следует предпочитать подходящие асинхронные API ввода-вывода.
Голодание цикла событий возникает, когда синхронный JavaScript или постоянно пополняемые очереди контрольных точек мешают своевременной обработке функций обратного вызова ввода-вывода, таймеров и запросов. Причины — тяжёлые вычисления, операции с большим JSON, рекурсивное планирование микрозадач и чрезмерное использование process.nextTick. Ограничивайте входные данные, профилируйте дорогие операции и переносите вычислительные задачи в worker_threads или другой сервис. При разбиении работы на части передавайте управление следующему обороту цикла событий, например через setImmediate: повторное ожидание уже разрешённых Promise или вызовы queueMicrotask по-прежнему могут мешать обработке ввода-вывода. Избегайте блокирующих синхронных API в часто выполняемых обработчиках запросов.
9Что такое функция обратного вызова и «ад обратных вызовов»?
Функция обратного вызова передаётся другой операции, которая затем её вызывает. Выполнение может быть синхронным, как в Array.map, или асинхронным, как в fs.readFile. Классические асинхронные функции обратного вызова Node.js обычно следуют соглашению «ошибка первым аргументом»: callback(error, result). «Ад обратных вызовов» — глубокая вложенность зависимых функций, затрудняющая понимание порядка выполнения и обработки ошибок. Именованные функции, небольшие операции, Promise и async/await улучшают читаемость. Независимые операции можно запускать совместно, например через Promise.all, соблюдая ограничения параллелизма и обрабатывая ошибки.
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
Promise представляет будущий результат async операции и имеет состояния pending, fulfilled или rejected. async-функция всегда возвращает Promise. await приостанавливает только текущую async-функцию, но не весь Node.js процесс. В хорошем ответе важно упомянуть try/catch, обработку rejected Promises, отсутствие висящих или необработанных Promise и Promise.all для независимой работы.
11Чем различаются process.nextTick, queueMicrotask, setImmediate и setTimeout?
process.nextTick планирует работу в очереди next-tick Node; queueMicrotask использует очередь микрозадач V8, в которой выполняются и реакции Promise. В обычной контрольной точке после кода верхнего уровня CommonJS функции nextTick идут перед поставленными в очередь функциями Promise/queueMicrotask. На верхнем уровне ES-модуля выполнение уже происходит в контексте микрозадачи, поэтому микрозадачи могут выполняться перед nextTick. Это зависит от контекста и не является универсальным правилом приоритета, прерывающим работающий JavaScript. setImmediate выполняется в check. setTimeout(fn, 0) ждёт минимального порога таймера и не выполняется немедленно. Их порядок зависит от места планирования; если оба запланированы внутри функции обратного вызова ввода-вывода, первым выполняется setImmediate. Для переносимого откладывания микрозадачи предпочтителен queueMicrotask, если не требуется специфическое поведение nextTick.
// CommonJS file (.cjs): nextTick precedes promise at this top level.
// ES module file (.mjs): promise precedes nextTick at this top level.
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// timeout vs immediate is context-dependent here.
Для синхронных исключений используйте try/catch. Операции на Promise ожидайте через await внутри try/catch либо назначайте обработчик отклонения; внешний синхронный try/catch не перехватывает исключение из функции обратного вызова, исполняемой позже. В функциях с ошибкой первым аргументом нужно проверять этот аргумент, а события error у EventEmitter требуют подходящих обработчиков. Серверу нужны централизованная обработка ошибок запросов, журналирование полезного контекста и защита внутренних сведений от раскрытия клиенту. После uncaughtException нельзя считать продолжение работы безопасным: выполните только необходимую синхронную аварийную очистку, завершите процесс и поручите перезапуск управляющей программе. Плановое корректное завершение полезно при обычной остановке, но произвольное асинхронное восстановление после неперехваченного исключения ненадёжно.
Модуль — изолированная единица кода с явно экспортируемым интерфейсом. В Node.js есть built-in modules вроде node:fs и node:http, локальные модули проекта, npm packages, CommonJS и ECMAScript modules. Модули помогают разделять ответственность и переиспользовать код. Production-компромисс — coupling: глубокие циклические импорты и side effects при import усложняют тестирование и startup behavior.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14Чем различаются require в CommonJS и import в ES Modules?
CommonJS использует require и module.exports; ES-модули — import и export. require обычно загружает синхронно и может вызываться условно. Статические объявления import открывают граф зависимостей для анализа, а динамический import() возвращает Promise. Современный Node.js также позволяет через require загрузить граф ES-модулей без await верхнего уровня, если соблюдены правила совместимости: ESM не означает обязательного асинхронного выполнения. Режим определяется расширениями .cjs/.mjs, полем type в package.json и применимыми правилами распознавания синтаксиса. ESM поддерживает await верхнего уровня. В CommonJS есть __dirname/__filename; в современном Node.js для файловых ES-модулей доступны import.meta.dirname/filename, а import.meta.url полезен для разрешения путей через URL и совместимости со старыми версиями.
// File: math.cjs (CommonJS)
function calculate(a, b) { return a + b; }
module.exports = { calculate };
// Separate file: math.mjs (ES module)
export function calculate(a, b) { return a + b; }
// These are two separate files, not one combined script.
15Что такое npm, package.json и package-lock.json?
npm — интерфейс командной строки для управления пакетами. package.json описывает метаданные, сценарии и требования к прямым зависимостям, часто диапазонами версий. package-lock.json фиксирует разрешённое дерево зависимостей с версиями пакетов и сведениями для проверки целостности. Файл блокировки приложения следует хранить в системе контроля версий. npm ci полезен в непрерывной интеграции: он устанавливает зависимости по файлу блокировки, удаляет существующий node_modules и завершается ошибкой при расхождении package.json с этим файлом. Файл блокировки уменьшает дрейф зависимостей, но не делает одинаковыми операционные системы, нативные сборки, сценарии жизненного цикла, версии Node и параметры установки. Воспроизводимая сборка также требует контролируемых инструментов и окружения.
16Как управлять конфигурацией и переменными окружения?
Переменные окружения доступны через process.env: присутствующие значения являются строками, отсутствующее свойство даёт undefined. При запуске проверяйте обязательные значения и осознанно преобразуйте типы; одного Number для PORT недостаточно, чтобы отклонить пустую строку, NaN или порт за пределами диапазона. При неверной обязательной конфигурации завершайте запуск с понятной ошибкой. Не храните реальные секреты в системе контроля версий; передавайте производственные секреты через платформу развёртывания или хранилище секретов. Пример конфигурации без секретов можно включать в репозиторий. NODE_ENV — соглашение приложения, а не доверенная граница безопасности.
const rawPort = process.env.PORT ?? '3000';
if (!/^\d+$/.test(rawPort)) throw new Error('PORT must be an integer');
const port = Number(rawPort);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
throw new Error('PORT must be between 1 and 65535');
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl?.trim()) throw new Error('DATABASE_URL is required');
EventEmitter реализует publish-subscribe внутри Node.js процесса. on регистрирует listener, once — listener на одно событие, off удаляет listener, emit синхронно вызывает listeners в порядке регистрации. Синхронность важна: медленный listener блокирует emitter. Событие error особое: необработанный error event может завершить процесс. В долгоживущих системах нужно удалять ненужные listeners, чтобы не создавать memory leaks.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('orderCreated', order => {
console.log(`Order ${order.id} created`);
});
emitter.emit('orderCreated', { id: 42 });
Buffer представляет фиксированную последовательность байтов. Он нужен для файлов, TCP sockets, HTTP bodies, images, crypto и binary protocols. Buffer.from создает buffer из данных, Buffer.alloc выделяет zero-filled память, Buffer.allocUnsafe быстрее, но может содержать остаточные данные из памяти до перезаписи. allocUnsafe безопасен только если код сразу полностью заполняет buffer и не раскрывает stale data.
const buffer = Buffer.from('Hello', 'utf8');
console.log(buffer.toString('utf8'));
const safe = Buffer.alloc(1024);
// Prefer alloc for general use. Never expose allocUnsafe bytes before filling them.
const fast = Buffer.allocUnsafe(1024);
fast.fill(0); // All bytes initialized before use.
Streams обрабатывают данные частями, не загружая все в память. Основные виды: Readable, Writable, Duplex и Transform. Примеры: файлы, HTTP request/response, TCP sockets, gzip streams. Streams полезны для больших файлов и сетевого трафика, потому что снижают memory usage и latency. В production важно корректно обрабатывать errors по всей цепочке.
Backpressure возникает, когда источник генерирует chunks быстрее, чем получатель может их обработать. Без backpressure память может расти до замедления или падения процесса. Streams и pipe умеют координировать flow, но pipeline из node:stream/promises безопаснее для сложных цепочек: он пробрасывает errors, закрывает связанные streams и возвращает Promise. Это хороший default для compression, uploads, downloads и transformations.
import fs from 'node:fs';
import zlib from 'node:zlib';
import { pipeline } from 'node:stream/promises';
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz')
);