Skip to content

Latest commit

 

History

History
1713 lines (1159 loc) · 74.1 KB

File metadata and controls

1713 lines (1159 loc) · 74.1 KB
layout ../../layouts/Layout.astro
title Node.js
description Node.js basics, npm scripts, frontend tooling, HTTP server, networking, observability и рабочее окружение
category Backend
kind questions
order 75

Node.js

Node.js basics

Что такое Node.js?

Node.js — JavaScript runtime вне браузера, построенный вокруг V8 и системных API. Он используется для servers, CLI, build tools и automation. Node предоставляет filesystem, processes и network APIs, которых нет в browser sandbox.

Что такое ES Modules в Node.js?

Это стандартная система import/export, включаемая расширением .mjs или "type": "module". Она поддерживает static analysis, top-level await и browser-compatible syntax. Resolution и interop с CommonJS имеют отдельные правила.

Что такое process.env?

Это объект environment variables текущего процесса. Значения являются строками или undefined, поэтому их нужно валидировать и преобразовывать. Секреты server process нельзя автоматически встраивать во frontend bundle.

Что такое child process?

Это отдельный процесс операционной системы, запущенный из Node через spawn, exec или fork. Он имеет отдельную память и может выполнять внешнюю команду. Нужно обрабатывать exit code, stderr, signals и размер output.

Чем Node.js отличается от браузера?

В Node нет DOM, window и browser security model, но доступны process, filesystem и server sockets. Browser имеет UI, Web APIs и origin restrictions. Общий JavaScript syntax не гарантирует совместимость окружений.

Почему Node.js называют event-driven runtime?

Программа регистрирует handlers, а runtime вызывает их при событиях I/O, timers или завершении задач. Это позволяет обслуживать много соединений без отдельного JavaScript thread на каждое. CPU-bound handler все равно блокирует event loop.

Чем require отличается от import?

require() — CommonJS function с синхронной загрузкой и runtime-вызовом. Static import анализируется до выполнения и работает в ESM. Dynamic import() асинхронен и доступен в обоих современных контекстах.

Что такое V8?

V8 — JavaScript engine от Chromium, который парсит, интерпретирует и JIT-компилирует код. Node использует V8 для выполнения ECMAScript. Browser APIs и Node APIs находятся вокруг engine, а не внутри языка.

Что такое libuv?

libuv — библиотека Node для event loop, async I/O, timers, filesystem и thread pool. Она скрывает различия операционных систем. Не все операции выполняются на главном JavaScript thread.

Что такое event loop в Node.js?

Event loop проходит фазы timers, pending callbacks, poll, check и close callbacks, обрабатывая готовые задачи. Microtasks выполняются между соответствующими этапами. Детали важны при диагностике starvation и порядка callbacks.

Чем event loop в Node.js отличается от browser event loop?

Оба координируют tasks и microtasks, но источники событий и фазы различаются. Browser привязан к rendering frames и Web APIs, Node — к libuv phases и server I/O. Нельзя механически переносить весь порядок callbacks между окружениями.

Что такое CommonJS?

CommonJS — историческая module system Node с require() и module.exports. Модули загружаются синхронно и имеют собственный wrapper scope. Экосистема постепенно переходит на стандартные ES Modules.

Что такое __dirname и почему его нет в ESM?

В CommonJS это directory текущего module file. ESM использует стандартный import.meta.url, из которого путь получают через fileURLToPath. Отсутствие __dirname связано с другой module model, а не с невозможностью работать с путями.

Что такое stream?

Stream обрабатывает данные частями вместо загрузки всего объема в память. Бывают readable, writable, duplex и transform streams. Backpressure предотвращает переполнение медленного consumer.

Что такое Buffer?

Buffer представляет последовательность bytes в Node и используется для файлов, sockets и binary protocols. Encoding нужно задавать явно при преобразовании в строку. В browser близкие primitives — ArrayBuffer и typed arrays.

Что такое worker_threads?

Worker threads выполняют JavaScript параллельно внутри одного Node process с отдельными isolates. Они подходят для CPU-bound вычислений, а не обычного async I/O. Обмен сообщениями и shared memory добавляют overhead.

Многопоточность и синхронизация

Однопоточен ли Node.js?

JavaScript-код внутри одного Node.js isolate обычно выполняется в одном потоке с одним event loop. Но сам runtime не является полностью однопоточным: V8, libuv, операционная система и thread pool могут выполнять работу в других потоках.

Для параллельного выполнения JavaScript-кода Node.js предоставляет worker_threads. Поэтому точнее говорить: основной JavaScript-поток однопоточен, но Node.js умеет использовать несколько потоков и процессов.

Чем concurrency отличается от parallelism?

Concurrency означает, что несколько задач находятся в работе одновременно и переключаются во времени. Например, один event loop может ожидать несколько HTTP-запросов, не блокируя выполнение программы.

Parallelism означает физическое выполнение нескольких задач в один момент времени на разных CPU cores. Для параллельного JavaScript-кода в Node.js обычно используют worker_threads или несколько процессов.

Асинхронность сама по себе не делает CPU-bound код параллельным. Тяжелый синхронный цикл все равно блокирует event loop.

Чем worker_threads отличаются от child_process и cluster?
  • worker_threads запускают JavaScript параллельно внутри одного процесса. У каждого worker свой V8 isolate и event loop, но workers могут обмениваться сообщениями и использовать общую память через SharedArrayBuffer.
  • child_process запускает отдельный процесс с отдельной памятью. Это полезно для изоляции, запуска внешних программ и независимого управления ресурсами.
  • cluster запускает несколько Node.js processes и помогает распределять входящие соединения одного server port между ними.

Для CPU-bound вычислений внутри приложения обычно подходит pool из worker_threads. Для сильной изоляции и масштабирования HTTP server по CPU cores часто используют несколько процессов или внешний process manager.

Как выполнить CPU-bound задачу в отдельном Worker Thread?

Основной поток создает Worker, передает входные данные и получает результат через message channel.

fibonacci-worker.mjs:

import {parentPort, workerData} from 'node:worker_threads';

const fibonacci = (value) => {
  if (value < 2) {
    return value;
  }

  return fibonacci(value - 1) + fibonacci(value - 2);
};

parentPort.postMessage(fibonacci(workerData));

main.mjs:

import {Worker} from 'node:worker_threads';

const runFibonacci = (value) =>
  new Promise((resolve, reject) => {
    const worker = new Worker(new URL('./fibonacci-worker.mjs', import.meta.url), {
      workerData: value,
    });

    worker.once('message', resolve);
    worker.once('error', reject);
    worker.once('exit', (code) => {
      if (code !== 0) {
        reject(new Error(`Worker завершился с кодом ${code}`));
      }
    });
  });

console.log(await runFibonacci(42));

Создавать новый worker для каждой маленькой задачи дорого. В production обычно используют постоянный worker pool и очередь задач.

Общая память и синхронизация

Как Worker Threads обмениваются данными?

Основные варианты:

  1. postMessage() копирует данные по правилам structured clone.
  2. ArrayBuffer можно передать через transfer list без копирования, после чего исходная сторона теряет доступ к buffer.
  3. SharedArrayBuffer доступен нескольким threads одновременно и требует явной синхронизации через Atomics.

Message passing обычно безопаснее и проще. Shared memory полезна только там, где стоимость копирования действительно существенна и команда готова управлять race conditions.

Что такое race condition в Node.js?

Race condition возникает, когда результат зависит от порядка параллельного доступа к общему состоянию. Например, операция counter[0] += 1 состоит из чтения, вычисления и записи. Два workers могут прочитать одно значение и потерять одно из увеличений.

Для счетчика в SharedArrayBuffer нужна атомарная операция:

const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const counter = new Int32Array(buffer);

Atomics.add(counter, 0, 1);

Для обычных async callbacks в одном event loop тоже возможны логические race conditions, если между чтением и записью есть await. Но Atomics решает только синхронизацию shared memory между threads, а не любую ошибку конкурентного доступа.

Что такое Atomics и зачем он нужен?

Atomics предоставляет неделимые операции над integer typed arrays, созданными поверх SharedArrayBuffer. Например:

  • Atomics.load() и Atomics.store() читают и записывают значение;
  • Atomics.add() атомарно изменяет счетчик;
  • Atomics.compareExchange() реализует compare-and-swap;
  • Atomics.wait() приостанавливает thread до изменения значения;
  • Atomics.notify() пробуждает ожидающие threads.

Atomics.wait() блокирует текущий thread, поэтому его не следует использовать в основном Node.js event loop. Обычно ожидание выполняют внутри worker thread.

Чем mutex отличается от semaphore?

Mutex разрешает вход в критическую секцию только одному участнику. Обычно освободить mutex должен тот же участник, который его захватил.

Semaphore хранит счетчик разрешений. Counting semaphore со значением N допускает одновременно до N участников. Binary semaphore со значением 1 похож на mutex, но семантика владения может отличаться.

Примеры:

  • mutex защищает изменение одного общего объекта;
  • semaphore ограничивает pool из четырех database connections;
  • semaphore ограничивает число одновременно выполняемых HTTP requests;
  • semaphore между workers ограничивает доступ к дефицитному shared resource.
Как реализовать асинхронный semaphore для ограничения Promise-задач?

Такой semaphore ограничивает concurrency внутри одного event loop. Он полезен для HTTP requests, файловых операций или доступа к connection pool, но не делает CPU-bound JavaScript параллельным.

class Semaphore {
  #available;
  #queue = [];

  constructor(limit) {
    if (!Number.isInteger(limit) || limit < 1) {
      throw new TypeError('Semaphore limit must be a positive integer');
    }

    this.#available = limit;
  }

  acquire() {
    if (this.#available > 0) {
      this.#available -= 1;

      return Promise.resolve(this.#createRelease());
    }

    return new Promise((resolve) => {
      this.#queue.push(resolve);
    });
  }

  async run(task) {
    const release = await this.acquire();

    try {
      return await task();
    } finally {
      release();
    }
  }

  #createRelease() {
    let released = false;

    return () => {
      if (released) {
        return;
      }

      released = true;

      const next = this.#queue.shift();

      if (next) {
        next(this.#createRelease());
      } else {
        this.#available += 1;
      }
    };
  }
}

Использование с максимум двумя одновременными запросами:

const semaphore = new Semaphore(2);
const urls = ['https://a.example', 'https://b.example', 'https://c.example'];

const responses = await Promise.all(urls.map((url) => semaphore.run(() => fetch(url))));

Освобождение находится в finally, поэтому permit вернется даже при ошибке задачи.

Как реализовать semaphore между Worker Threads через SharedArrayBuffer?

В общей памяти можно хранить количество доступных permits. Захват выполняется через compare-and-swap, а ожидающие workers блокируются через Atomics.wait().

shared-semaphore.mjs:

export const acquire = (state) => {
  while (true) {
    const permits = Atomics.load(state, 0);

    if (permits > 0 && Atomics.compareExchange(state, 0, permits, permits - 1) === permits) {
      return;
    }

    Atomics.wait(state, 0, 0);
  }
};

export const release = (state) => {
  Atomics.add(state, 0, 1);
  Atomics.notify(state, 0, 1);
};

Основной поток создает shared state и передает его workers:

import {Worker} from 'node:worker_threads';

const permits = 2;
const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const state = new Int32Array(buffer);

Atomics.store(state, 0, permits);

const workers = Array.from(
  {length: 4},
  (_, id) =>
    new Worker(new URL('./worker.mjs', import.meta.url), {
      workerData: {id, semaphoreBuffer: buffer},
    }),
);

await Promise.all(
  workers.map(
    (worker) =>
      new Promise((resolve, reject) => {
        worker.once('exit', resolve);
        worker.once('error', reject);
      }),
  ),
);

worker.mjs:

import {setTimeout} from 'node:timers/promises';
import {workerData} from 'node:worker_threads';
import {acquire, release} from './shared-semaphore.mjs';

const state = new Int32Array(workerData.semaphoreBuffer);

acquire(state);

try {
  console.log(`Worker ${workerData.id} получил permit`);
  await setTimeout(500);
} finally {
  release(state);
}

Этот пример учебный. В production нужно дополнительно продумать shutdown, timeout, cancellation, падение worker после acquire(), максимальное число permits, fairness и восстановление утраченного permit.

Какие ошибки часто допускают при работе с многопоточностью в Node.js?
  • отправляют обычный async I/O в workers, хотя event loop уже эффективно обрабатывает его;
  • создают новый worker для каждой маленькой задачи вместо worker pool;
  • копируют большие данные между threads и теряют выигрыш на serialization;
  • изменяют SharedArrayBuffer без Atomics;
  • вызывают blocking primitives в основном event loop;
  • забывают освобождать mutex или semaphore в finally;
  • не обрабатывают error, exit, timeout и отмену worker;
  • предполагают, что semaphore автоматически гарантирует fairness и отсутствие deadlock.

По умолчанию лучше использовать message passing и изолированное состояние. Shared memory и низкоуровневые primitives нужны только после измерений и при понятной модели владения ресурсами.

npm и package scripts

Что такое npm package?

Это directory или опубликованный artifact с package.json и кодом. Package может быть библиотекой, CLI или приложением. Имя и version определяют его identity в registry.

Что такое package.json?

Manifest проекта описывает metadata, scripts, dependencies, package exports и настройки tools. Он не фиксирует точное дерево transitive dependencies. Для этого нужен lock-файл.

Что такое peerDependencies?

Они объявляют, что package ожидает совместимую dependency от host-проекта. Это важно для frameworks и plugins, которым нужен общий runtime instance. Например, Angular library обычно указывает Angular как peer dependency.

Что такое npm scripts?

Это именованные shell-команды в поле scripts, запускаемые через npm run. npm добавляет локальные binaries из node_modules/.bin в PATH. Lifecycle scripts могут запускаться автоматически, поэтому dependencies должны быть доверенными.

Что такое semantic versioning?

SemVer использует major.minor.patch: major для breaking changes, minor для совместимой функциональности, patch для совместимых fixes. До 1.0.0 гарантии часто трактуются осторожнее. Versioning полезно только при честном публичном контракте.

Что значит ^, ~ и exact version в package.json?

Exact устанавливает только указанную version. ~1.2.3 допускает patch updates, ^1.2.3 — compatible minor и patch до следующего major. Фактическую установленную версию фиксирует lock-файл.

Чем dependencies отличаются от devDependencies?

dependencies нужны package во время runtime потребителя, devDependencies — для разработки, тестов и сборки. Для frontend application обе группы обычно устанавливаются перед build. Для library правильная классификация влияет на потребителя.

Что такое package-lock.json?

Lock-файл фиксирует точные версии и integrity всего установленного dependency tree. Он делает installs воспроизводимыми. Ручное редактирование обычно не требуется.

Почему lock-файл важно коммитить?

CI и разработчики получают одно и то же разрешенное дерево, а изменения dependencies видны в review. Без lock-файла compatible ranges могут установить разные transitive versions. Это усложняет debugging и supply-chain audit.

Что делает npm install?

Разрешает dependencies, устанавливает их в node_modules и обновляет lock-файл при необходимости. Он подходит для локальной разработки и добавления packages. Результат может изменить lock при рассинхронизации manifest.

Что делает npm ci?

Удаляет существующий node_modules и устанавливает точно по lock-файлу. Команда завершится ошибкой, если package.json и lock не согласованы. Она не переписывает dependency tree.

Чем npm ci лучше для CI?

Дает чистую, воспроизводимую установку и быстро обнаруживает незакоммиченный lock update. Поведение меньше зависит от предыдущего workspace state. Кеш npm downloads можно использовать отдельно от node_modules.

Как npm scripts используются во frontend-проектах?

Для start, build, test, lint, format, code generation и deployment wrappers. Они дают команде единый interface поверх Angular CLI, Vite и других tools. Scripts должны оставаться понятными и composable.

Чем task runner отличается от package scripts?

Task runner вроде Gulp или Grunt описывает pipeline через отдельный инструмент и плагины. Package scripts запускают команды напрямую через npm, pnpm, Yarn или Bun. В современных frontend-проектах многие задачи закрываются framework CLI, bundler и package scripts, но legacy-проекты могут все еще использовать task runners для сборки assets или интеграции.

Node.js для frontend tooling

Что такое dev server?

Локальный HTTP server для разработки с module transforms, source maps, watch и hot reload. Он оптимизирован для feedback, а не безопасности, кеширования и production traffic. Его нельзя использовать как production hosting.

Почему frontend-проекту нужен Node.js?

Node запускает package manager, compiler, linter, tests, dev server и production build. Результат затем выполняется в browser и может не зависеть от Node. SSR-приложение дополнительно использует Node во время runtime.

Как Node.js используется в Angular CLI?

CLI запускается как Node program, читает workspace config и вызывает builders. TypeScript/Angular compiler, dev server и test runner также работают в Node. Поддерживаемая Node version зависит от Angular version.

Как Node.js используется в Vite/Webpack/esbuild?

Он запускает bundler process, читает файлы, разрешает modules и обслуживает plugins. Vite предоставляет dev server, Webpack строит module graph, esbuild выполняет быстрые transforms/bundling. Production output предназначен для browser или server target.

Чем dev server отличается от production build?

Dev server часто преобразует modules по запросу и хранит часть данных в памяти. Production build минифицирует, хеширует, оптимизирует и записывает deployable artifacts. Поведение окружения нужно проверять production build.

Почему код, который работает в Node.js, может не работать в браузере?

Browser не предоставляет fs, process, CommonJS resolution и unrestricted sockets. Также действуют CORS, CSP и sandbox. Bundler polyfills не следует считать автоматическими или бесплатными.

Почему код, который работает в браузере, может не работать в Node.js?

В Node обычно нет window, document, DOM, layout и browser storage. SSR-код должен изолировать browser-only API. Некоторые Web APIs появляются в новых Node versions, но их поддержку нужно проверять.

Когда scaffolding tools полезны, а когда вредят?

Scaffolding tools полезны, когда создают повторяемую структуру components, features, libraries или tests по текущим командным conventions. Они вредят, если генерируют много непонятного boilerplate или закрепляют устаревшую архитектуру. Генератор должен отражать живую codebase, а не исторические привычки.

HTTP server, networking и curl

Что такое idempotency?

Операция идемпотентна, если повтор одного и того же запроса имеет тот же ожидаемый итоговый эффект. Это важно для retries при сетевой неопределенности. API может использовать idempotency key для безопасного повторения создания платежа.

Что такое HSTS и зачем браузер проверяет HSTS list?

HSTS (HTTP Strict Transport Security) говорит браузеру обращаться к сайту только по HTTPS. Если domain есть в preloaded HSTS list или браузер уже получил Strict-Transport-Security, попытка http:// будет локально повышена до https:// до сетевого запроса.

Это защищает от downgrade attack и случайной отправки cookies по незащищенному соединению. Для frontend это важно при диагностике "почему HTTP redirect не виден в Network" и при настройке production-доменов.

Что такое ETag?

ETag — validator версии ресурса. Клиент может отправить If-None-Match, и сервер вернет 304 Not Modified, если ресурс не изменился. Это экономит body transfer, но оставляет network round trip, поэтому для content-hashed assets часто выгоднее долгий cache lifetime.

Что такое Expires?

Expires задает абсолютную дату, после которой response считается устаревшим. В современных приложениях чаще предпочитают Cache-Control, потому что относительные правила проще и надежнее при разнице часов между системами. Если оба header присутствуют, Cache-Control обычно имеет приоритет.

Из чего состоит HTTP request?

Request line содержит method, target и HTTP version, затем идут headers и optional body. URL включает path и query params. Host, authorization и content metadata передаются headers.

Из чего состоит HTTP response?

Status line содержит HTTP version и status code, затем идут response headers и optional body. Headers описывают content, cache, cookies и transport metadata. Body может содержать JSON, HTML, файл или stream.

Чем OPTIONS и HEAD отличаются от основных HTTP methods?

OPTIONS запрашивает поддерживаемые возможности ресурса или используется браузером для CORS preflight. HEAD похож на GET, но возвращает только headers без body, что полезно для проверки metadata, cache и доступности ресурса. Оба метода важны для инфраструктуры, но редко являются основными product actions.

Какие HTTP methods обычно идемпотентны?

GET, HEAD, PUT, DELETE и OPTIONS определены как идемпотентные по семантике, PATCH может быть таким по контракту, POST обычно нет. Идемпотентность не означает одинаковый response: DELETE повторно может вернуть другой status.

Как использовать curl для проверки HTTP API?

curl позволяет отправить запрос вне приложения и увидеть, проблема находится в API, сети или frontend-коде.

curl https://api.example.com/users
curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Max","role":"frontend"}'
curl -X PATCH https://api.example.com/users/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer token" \
  -d '{"name":"Alex"}'
Чем HTTP отличается от HTTPS?

HTTPS передает HTTP внутри защищенного TLS-соединения. Оно шифрует трафик, подтверждает подлинность сервера сертификатом и защищает данные от незаметного изменения в пути.

HTTPS не исправляет XSS, слабую авторизацию или утечку данных на сервере.

Как устроены TCP/IP и HTTP?

Упрощенная модель TCP/IP:

  1. Прикладной уровень: HTTP, DNS, WebSocket.
  2. Транспортный уровень: TCP или UDP.
  3. Сетевой уровень: IP и маршрутизация пакетов.
  4. Канальный уровень: передача кадров внутри локальной сети.

HTTP — протокол прикладного уровня с моделью request/response. Клиент отправляет метод, URL, заголовки и при необходимости body; сервер возвращает status code, заголовки и body.

HTTPS — HTTP поверх защищенного TLS-соединения. TCP обеспечивает надежную упорядоченную доставку для HTTP/1.1 и HTTP/2; HTTP/3 использует QUIC поверх UDP.

TCP устанавливает соединение, гарантирует порядок и повторную доставку потерянных данных. UDP отправляет datagrams без таких гарантий, но с меньшими накладными расходами.

В модели OSI HTTP относится к прикладному уровню, TCP/UDP - к транспортному, IP - к сетевому.

Для frontend-разработчика важны методы, коды ответа, заголовки, кеширование, cookies, CORS, TLS, сжатие и понимание того, что количество и размер запросов влияют на производительность.

Как работает DNS lookup?

DNS lookup преобразует hostname в IP-адрес. Браузер и ОС сначала проверяют свои кеши, затем resolver обращается к настроенному DNS-серверу, который может вернуть ответ из кеша или выполнить recursive lookup через DNS hierarchy.

Ответы имеют TTL, поэтому смена IP, CDN или DNS-записей может доходить до пользователей не мгновенно. В DevTools это видно как DNS timing, если соединение не было переиспользовано.

Зачем frontend-разработчику понимать DNS?

DNS влияет на latency первого запроса к origin, API, CDN, fonts и analytics. Много разных domains увеличивает число DNS lookup, TCP/TLS setup и риск частичных сбоев.

Практически это помогает объяснять медленный first visit, выбирать preconnect только для действительно критичных origins, диагностировать проблемы после смены CDN, CNAME или окружения API.

Что происходит перед отправкой HTTP-запроса по HTTPS?

Браузер открывает или переиспользует соединение с origin. Если нового соединения избежать нельзя, сначала устанавливается transport connection, затем выполняется TLS handshake: выбирается версия TLS и cipher suite, проверяется certificate chain, согласуются ключи шифрования и имя сервера через SNI.

Только после успешного TLS handshake отправляется HTTP-запрос. Поэтому медленный HTTPS может быть связан не с API handler, а с network latency, certificate проблемами, отсутствием connection reuse или слишком большим числом origins.

Чем TCP отличается от UDP в контексте HTTP?

TCP дает надежный упорядоченный byte stream: потерянные данные повторно передаются, а приложение получает их в порядке. HTTP/1.1 и HTTP/2 обычно работают поверх TCP.

UDP не гарантирует доставку и порядок datagrams. HTTP/3 использует QUIC поверх UDP, добавляя надежность, шифрование и мультиплексирование на своем уровне. Для frontend это проявляется в timing, connection setup и поведении при packet loss.

Что происходит, если сетевой пакет потерялся?

Для TCP потеря приводит к повторной передаче и увеличению latency; данные выше по стеку будут ждать недостающий кусок. В HTTP/2 поверх TCP потеря может задержать несколько streams на одном соединении.

В QUIC/HTTP/3 потеря одного stream меньше блокирует остальные streams, но пользователь все равно видит задержку, timeout или retry на уровне приложения. Frontend должен проектировать loading, timeout и idempotent retry с учетом этой неопределенности.

Как выглядит минимальный HTTP request?

Минимально важны method, request target, HTTP version и Host для HTTP/1.1:

GET /products?limit=10 HTTP/1.1
Host: example.com
Accept: text/html

Реальный browser request добавит headers вроде User-Agent, Accept-Encoding, Cookie, Sec-Fetch-*, If-None-Match или Origin. В HTTP/2 и HTTP/3 формат передачи другой, но семантика method, path, headers и body сохраняется.

Что означают Date и Age в HTTP headers?

Date показывает время формирования response на сервере или proxy. Age показывает, сколько секунд response уже находится в cache, например в CDN. Эти headers помогают понять, пришел ли ответ из cache и насколько он свежий.

Почему раньше ассеты разносили по нескольким доменам?

В HTTP/1.1 браузеры ограничивали число одновременных соединений к одному host, поэтому domain sharding помогал качать больше файлов параллельно. Цена — дополнительные DNS lookup, TLS handshakes, cookies и сложность кеширования. В современных проектах этот прием чаще вреден.

Что такое domain prefetch?

Domain prefetch заранее выполняет DNS lookup для origin, который скоро может понадобиться. Это может сократить задержку первого запроса к стороннему домену, но лишние hints расходуют ресурсы и могут раскрывать будущие переходы. Для более критичных ресурсов иногда уместнее preconnect.

Что происходит, если HTML ссылается на ресурсы с других доменов?

Для каждого нового origin браузеру могут понадобиться отдельные DNS lookup, connection setup и TLS handshake. CSS, scripts, images, fonts и API-запросы также подчиняются mixed content, CORS, CORP/CORB, CSP и cookie rules.

Практические проблемы: медленные third-party scripts задерживают render, fonts могут блокировать текст, API может упасть из-за CORS, а cookies не отправятся без правильных SameSite, Secure и credentials settings.

Что делает сервер после получения HTTP request?

Server или edge layer принимает request, разбирает method, path, headers и body, применяет routing, authentication, authorization, validation, business logic и обращение к данным. Затем формирует status code, response headers и body.

До application code запрос могут обработать CDN, reverse proxy, WAF, cache или load balancer. Поэтому frontend диагностика должна смотреть status, headers, timing, trace id и то, на каком слое возникла ошибка.

Из чего состоят HTTP-запрос и ответ?

Запрос содержит method, URL, headers и необязательный body. Ответ содержит status code, headers и необязательный body.

Частые headers: Content-Type, Accept, Authorization, Cache-Control, ETag, Cookie, Set-Cookie, Origin. Формат body описывает Content-Type.

Чем WebSocket отличается от SSE?

WebSocket предоставляет постоянный двусторонний канал и подходит для чатов, multiplayer и совместного редактирования.

SSE передает события только от сервера к клиенту поверх HTTP, автоматически переподключается и проще для уведомлений, прогресса и live feed. SSE передает текстовые события и имеет browser-specific ограничения соединений.

Когда выбрать polling, SSE или WebSocket?
  • Polling прост и подходит для редких обновлений, когда задержка допустима.
  • SSE выбирают для постоянного потока server-to-client.
  • WebSocket нужен для частого двустороннего обмена с низкой задержкой.

Учитывают инфраструктуру, reconnect, authentication, масштабирование, mobile network и реальную частоту событий.

Чем Long Polling, WebSocket и Server-Sent Events отличаются?

Long Polling держит HTTP-запрос открытым до появления события, затем клиент сразу открывает следующий запрос. SSE дает постоянный однонаправленный поток server-to-client поверх HTTP и хорошо подходит для уведомлений. WebSocket открывает двусторонний канал с низкой задержкой, но требует отдельного управления reconnect, auth, scaling и проксированием.

Что такое 304 Not Modified и как он связан с кешем?

304 Not Modified означает, что cached response в browser HTTP cache еще можно использовать. Браузер отправляет conditional request с If-None-Match или If-Modified-Since, а сервер отвечает 304 без полного body.

Это экономит bandwidth, но не убирает network round trip. Для hashed assets часто лучше долгий Cache-Control: immutable, а для index.html — revalidation, чтобы быстрее получать новую версию приложения.

Что такое Cache-Control?

Cache-Control задает правила кеширования для browser cache, CDN и промежуточных caches: max-age, s-maxage, no-cache, no-store, public, private, immutable. Для hashed assets обычно используют долгий max-age и immutable, а для HTML SPA — короткое кеширование или revalidation, чтобы быстрее доставлять новые версии и rollback.

Для чего нужны If-Modified-Since и If-None-Match?

Это conditional request headers для revalidation. If-Modified-Since сравнивает дату изменения ресурса, а If-None-Match сравнивает ETag и обычно точнее. Если ресурс не изменился, сервер отвечает 304 без body, сохраняя bandwidth.

Что такое Transfer-Encoding?

Transfer-Encoding описывает, как body передается по соединению, например chunked encoding в HTTP/1.1. Chunked response позволяет начинать отправку до того, как известен полный размер body. В HTTP/2 и HTTP/3 framing устроен иначе, но семантика streaming response остается важной.

Как HTTP/2 и HTTP/3 изменили подход к domain sharding?

HTTP/2 добавил multiplexing нескольких streams в одном соединении, а HTTP/3 поверх QUIC уменьшает влияние части transport-level задержек. Поэтому много доменов обычно хуже: теряется reuse соединения и усложняется приоритизация. Оптимизация сместилась к правильному кешированию, размеру ресурсов и CDN, а не к искусственному дроблению origins.

Что такое клиент-серверная архитектура?

Клиент отвечает за интерфейс и отправляет запросы, сервер хранит данные, применяет бизнес-правила и возвращает ответы.

Граница не является границей доверия: server всегда повторно проверяет authentication, authorization и входные данные, даже если frontend уже выполнил validation.

Чем CDN cache отличается от browser cache?

Browser cache находится у конкретного пользователя и ускоряет повторные посещения. CDN cache находится ближе к пользователям на edge-узлах и снижает latency, трафик до origin и нагрузку на backend.

Для static assets обычно используют content hash и долгий Cache-Control. Для HTML чаще нужен короткий TTL или revalidation, чтобы пользователь быстро получил новую версию приложения. Важно понимать cache key: URL, headers, cookies и query params могут менять попадание в кеш.

Cache-Control: public, max-age=31536000, immutable

Node.js observability и RPS monitoring

Что такое RPS?

RPS, или Requests Per Second, — количество запросов, которое сервер обрабатывает за секунду. Например, 120 RPS означает, что сервер в среднем обрабатывает 120 HTTP-запросов в секунду.

Что такое RPS monitor?

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

Зачем frontend-разработчику понимать RPS?

Frontend-разработчик может работать с SSR, BFF, dev server, Node.js tooling и API-интеграциями. RPS помогает оценивать нагрузку на SSR, rate limits, кеширование, retry-логику и влияние frontend-кода на backend.

Как может выглядеть простой RPS monitor в Node.js?

Этот учебный HTTP-сервер считает запросы за последнюю секунду и выводит результат в консоль:

import http from 'node:http';

let requests = 0;

setInterval(() => {
  console.log(`RPS: ${requests}`);
  requests = 0;
}, 1000);

const server = http.createServer((request, response) => {
  requests += 1;

  response.writeHead(200, {'Content-Type': 'application/json'});
  response.end(JSON.stringify({ok: true}));
});

server.listen(3000, () => {
  console.log('Server started on http://localhost:3000');
});
Как проверить RPS monitor через curl?

После запуска сервера можно отправить несколько запросов вручную:

curl http://localhost:3000
curl http://localhost:3000
curl http://localhost:3000

В следующем секундном интервале сервер выведет количество полученных запросов.

Почему такой RPS monitor не production-ready?

Он хранит счетчик только в памяти одного процесса и теряет данные при перезапуске. Несколько процессов или containers будут считать RPS независимо. В production метрики агрегируют через Prometheus, OpenTelemetry, APM или другую систему observability и визуализируют, например, в Grafana.

Какие метрики кроме RPS важны для Node.js сервера?
  • Latency и p95/p99 response time.
  • Error rate и количество ответов 4xx/5xx.
  • CPU и memory usage.
  • Event loop delay.
  • Active connections.
  • Throughput.

Метрики полезно связывать между собой: один RPS не объясняет состояние системы.

Чем RPS отличается от latency?

RPS показывает количество обработанных запросов в секунду, а latency — время обработки отдельного запроса. Высокий RPS сам по себе не является проблемой, но рост latency и error rate вместе с ним может указывать на перегрузку.

Что такое p95 и p99 latency?

p95 — значение, быстрее которого завершились 95% запросов; p99 аналогично описывает 99% запросов. Перцентили лучше среднего показывают медленный хвост распределения, хотя для полной картины также нужны размер выборки и временное окно.

Как event loop delay связан с производительностью Node.js?

JavaScript выполняется в основном потоке Node.js. CPU-bound работа и долгие синхронные операции блокируют event loop, из-за чего callbacks и обработчики запросов запускаются позже. Event loop delay измеряет эту задержку и помогает найти такие блокировки.

Что может увеличить RPS capacity Node.js приложения?
  • Кеширование и использование reverse proxy или CDN.
  • Уменьшение CPU-bound работы в request handler.
  • Горизонтальное масштабирование и несколько процессов.
  • Оптимизация запросов к базе и connection pooling.
  • Уменьшение лишнего logging на горячем пути.

Изменения нужно подтверждать нагрузочными тестами: увеличение RPS не должно ухудшать latency и error rate сверх SLO.

Project tooling и рабочее окружение

Как package.json связан с рабочим окружением?

package.json — manifest Node.js-проекта. В рабочем окружении важно понимать, какие scripts запускать, какой package manager закреплен в проекте и какой lock-файл должен быть единственным источником воспроизводимой установки.

Подробности про manifest, зависимости, lock-файлы и npm-команды см. в разделе npm и package scripts.

Зачем frontend-проекту единый tooling?

Единый tooling снижает расхождения между локальной разработкой, CI и production build. Команда должна понимать, какой package manager используется, какие scripts являются основными, как запускать tests, lint, format, build и preview. Это ускоряет onboarding и уменьшает проблемы класса works on my machine.

Чем отличаются Bun, pnpm, Yarn и npm?

Все эти инструменты устанавливают зависимости, работают с package.json, запускают scripts и фиксируют результат через lock-файл. Отличаются скоростью, моделью node_modules, строгостью dependency resolution, зрелостью экосистемы и дополнительными возможностями.

  • npm поставляется вместе с Node.js, использует package-lock.json, хорошо поддерживается большинством инструментов и понятен почти всем разработчикам. Плюсы: стандарт де-факто, простая интеграция, минимум сюрпризов. Минусы: обычно медленнее pnpm и Bun, а node_modules может занимать больше места.
  • pnpm использует общий content-addressable store и связывает зависимости в node_modules через links. Плюсы: быстрая установка, экономия диска, строгая модель зависимостей, удобство для monorepo. Минусы: некоторые старые или некорректно написанные packages могут зависеть от плоского node_modules и требовать настройки.
  • Yarn использует yarn.lock, поддерживает workspaces и может работать как с node_modules, так и с Plug'n'Play. Плюсы: сильные monorepo-workflows, гибкие политики установки, хороший контроль зависимостей. Минусы: различия между Yarn Classic и современным Yarn, а Plug'n'Play может требовать дополнительной настройки IDE, bundler и отдельных инструментов.
  • Bun — более широкий toolkit: runtime, package manager, test runner и bundler. Плюсы: очень быстрая установка, удобные встроенные инструменты, совместимость с npm registry. Минусы: экосистема и совместимость с отдельными Node.js packages, native addons или enterprise tooling могут быть менее предсказуемыми, чем у npm/pnpm/Yarn.

Практическое правило: в одном проекте выбирают один package manager, фиксируют его через поле packageManager, коммитят ровно один lock-файл и используют одинаковую команду в CI. Смешивание package-lock.json, pnpm-lock.yaml, yarn.lock и bun.lock делает установку непредсказуемой.

Зачем нужен package-lock.json?

package-lock.json нужен для воспроизводимой установки npm-зависимостей в локальной разработке и CI.

Canonical-ответ: Что такое package-lock.json?

Чем npm install отличается от npm ci?

В рабочем процессе важно правило: локально чаще используют установку по правилам проекта, а в CI предпочитают воспроизводимую установку по lock-файлу.

Подробное сравнение см. в Node.js-разделе: Что делает npm install? и Что делает npm ci?.

Чем ESLint, Stylelint и Prettier отличаются?

ESLint анализирует код и находит потенциальные ошибки, небезопасные конструкции и нарушения правил проекта.

Stylelint выполняет похожую проверку для CSS, SCSS и других стилей. Prettier отвечает за форматирование: отступы, переносы и кавычки, но не заменяет semantic linting.

Вместе с EditorConfig эти инструменты автоматизируют механические правила и уменьшают споры в review. Важно запускать их локально и в CI, иначе guidelines остаются документом без enforcement.

Как вы поддерживаете единый стиль кода в команде?

Команда фиксирует правила в formatter, linting, editor config, pull request checklist и CI. Важно автоматизировать механические споры, а в review обсуждать читаемость, архитектуру и поведение. Если правило не проверяется автоматически, его стоит описать в локальных соглашениях или пересмотреть.

Для чего нужен linting tool?

Linting tool находит ошибки и risky patterns до runtime: неиспользуемый код, неправильные imports, нарушение accessibility, небезопасные конструкции и несогласованный стиль. Он не заменяет тесты и review, но дает быстрый feedback и делает качество менее зависимым от памяти конкретного разработчика.

Чем Webpack отличается от Vite?

Webpack строит dependency graph и создает bundles через loaders и plugins. Он гибкий, но development-сборка крупного проекта может быть тяжелой.

Vite в development использует native ES modules и преобразует файлы по запросу, поэтому обычно быстрее запускается и обновляет модули. Production-сборка использует отдельный bundling pipeline. В Angular конкретный инструмент часто скрыт за CLI builder.

Как код попадает в отдельный chunk?

Основные сигналы для сборщика:

  • отдельная entry point;
  • динамический import();
  • lazy route;
  • Angular @defer;
  • правила code splitting в конфигурации сборщика.

Сборщик анализирует dependency graph и выносит асинхронную или общую часть в отдельный файл. Chunk полезен, если уменьшает initial load, но слишком мелкое дробление увеличивает число запросов и служебные расходы.

Workflow и onboarding

Как выглядит обычный workflow при разработке страницы или фичи?

Сначала уточняют цель, состояния UI, acceptance criteria, API-контракт, accessibility и способ проверки. Затем делают минимальный инкремент, запускают локальные проверки, добавляют нужные тесты и отправляют маленький pull request. Хороший workflow оставляет следы: описание решения, известные ограничения и понятные шаги проверки.

Какое рабочее окружение вы предпочитаете?

Хороший ответ описывает не любимую IDE как самоцель, а условия продуктивности: быстрый запуск проекта, стабильный Node.js version, корректные extensions, formatter on save, debugger, терминал и доступ к DevTools. Важно уметь подстроиться под командные стандарты, чтобы локальная среда не расходилась с CI.

Что вы делаете, если проект использует tabs, а вы привыкли к spaces?

Нужно следовать проектным настройкам, а не личной привычке. Форматирование должны задавать .editorconfig, Prettier или другой formatter, чтобы файлы не менялись случайно из-за IDE. Если правила мешают, их обсуждают отдельно, а не меняют попутно в feature pull request.

Зачем frontend-команде documentation guidelines?

Documentation guidelines объясняют, где описаны архитектура, conventions, design system, onboarding, troubleshooting и решения по workflow. Без этого знания остаются в головах отдельных людей, а новые разработчики повторяют старые ошибки. Практичный подход — обновлять docs вместе с кодом в том же pull request.

Какие инструменты должны быть описаны в onboarding документации?

Нужно описать Node.js version, package manager, install command, dev server, build, tests, lint, format, environment variables, mock API и troubleshooting. Хорошая onboarding документация позволяет новому разработчику поднять проект без долгих личных созвонов и не расходиться с CI.

Frontend tooling

Чем Babel, SWC, esbuild и TypeScript compiler отличаются в frontend toolchain?

TypeScript compiler умеет type checking и emit, но в современных frontend-сборках часто разделяют type checking и transpile. Babel трансформирует syntax через plugins, SWC и esbuild делают быстрые transforms на native-реализациях.

Важно различать:

  • transpilation - преобразование syntax;
  • type checking - проверка типов;
  • bundling - построение graph и output chunks;
  • minification - уменьшение output.

Angular CLI скрывает большую часть деталей, но разработчик должен понимать, где искать проблему: в tsconfig, bundler, polyfills, browserslist или plugin pipeline.

{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "vite build"
  }
}
Как объяснить Vite, Webpack и Rollup на интервью?

Webpack - универсальный bundler с большим plugin ecosystem и исторически богатой настройкой. Rollup силен для libraries и tree shaking ES modules. Vite использует быстрый dev server на native ESM и собирает production через Rollup-пайплайн или framework-specific integration.

Хороший ответ не сводится к "Vite быстрее". Нужно объяснить dev server, HMR, module graph, code splitting, CSS/assets pipeline, source maps и то, как framework CLI ограничивает или расширяет доступ к настройкам.

export default {
  build: {
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
      },
    },
  },
};