Какие особенности runtime, type system или memory model JavaScript реально влияют на архитектуру приложения?
Event loop делает Node.js I/O-эффективным, но синхронный CPU-код блокирует всё; прототипная типизация требует runtime-валидации (Zod); GC паузы V8 критичны для latency-sensitive сервисов.
Event Loop и однопоточный runtime
V8 выполняет JavaScript в одном потоке. Event loop состоит из фаз: timers → pending callbacks → idle/prepare → poll → check (setImmediate) → close callbacks. Это архитектурно важно: блокирующая операция в главном потоке (синхронный fs.readFileSync, тяжёлый JSON.parse на 50 MB) замораживает весь сервер.
// Плохо: блокирует event loop
const data = fs.readFileSync('/large-file.json'); // блокирует на 200ms
JSON.parse(data.toString()); // ещё 100ms — все запросы ждут
// Хорошо: не блокирует
const data = await fs.promises.readFile('/large-file.json');
const parsed = await new Promise(resolve =>
setTimeout(() => resolve(JSON.parse(data)), 0) // или Worker Thread
);
Архитектурное следствие: CPU-intensive задачи выносятся в Worker Threads (node:worker_threads) или отдельные процессы. BFF-серверы, WebSocket-хабы и API-шлюзы идеально ложатся на event loop модель.
Прототипная система типов и dynamic typing
JavaScript использует прототипное наследование и динамическую типизацию. Это влияет на архитектуру: без TypeScript контракты между модулями держатся только на конвенциях и тестах. V8 оптимизирует «горячий» код через JIT, но только если объект не меняет форму (shape/hidden class). Добавление свойств после создания объекта деоптимизирует путь выполнения.
// TypeScript на этапе компиляции ловит контрактные ошибки
interface User {
id: string;
email: string;
}
function sendEmail(user: User): void {
// В runtime это всё равно JS — нет гарантии, что user.email существует
// если данные пришли из внешнего API без валидации
}
// Zod добавляет runtime-валидацию:
import { z } from 'zod';
const UserSchema = z.object({ id: z.string(), email: z.string().email() });
const user = UserSchema.parse(apiResponse); // throws если схема не совпала
Garbage Collection и memory model
V8 использует generational GC: young generation (Scavenge, ~1-2ms паузы) и old generation (Mark-Sweep/Mark-Compact, до 100ms паузы). Это критично для real-time приложений. Memory leaks в Node.js чаще всего возникают через:
- Замыкания, удерживающие большие объекты (event listeners не удалены через removeEventListener)
- Глобальные кеши без TTL или ограничения размера
- Циклические ссылки с native объектами (не мешают GC в V8, но могут в старых движках)
// Утечка памяти: listener не убирается
class DataProcessor extends EventEmitter {
start() {
// Каждый вызов start() добавляет listener без удаления предыдущего
someGlobalEmitter.on('data', this.handleData.bind(this));
}
}
// Правильно:
start() {
this.boundHandler = this.handleData.bind(this);
someGlobalEmitter.on('data', this.boundHandler);
}
stop() {
someGlobalEmitter.off('data', this.boundHandler);
}
Асинхронная модель и Promise chain
Microtask queue (Promise callbacks, queueMicrotask) выполняется между фазами event loop и имеет приоритет над macrotasks (setTimeout, setImmediate). Бесконечная цепочка resolved promises заблокирует event loop так же, как синхронный код.
// Потенциальная проблема: рекурсивные promises без yield
async function infiniteLoop() {
while (true) {
await Promise.resolve(); // microtask — не даёт I/O callbacks выполниться!
}
}
// Правильно: явный yield через setImmediate
async function safeLoop() {
while (true) {
await new Promise(r => setImmediate(r)); // даёт event loop пройти цикл
}
}
Подводные камни
- Считать, что async/await делает код параллельным — await сериализует, Promise.all() параллелизует.
- Игнорировать unhandledRejection — в Node.js 15+ это завершает процесс; всегда вешать process.on('unhandledRejection').
- Мутировать объекты между Worker Threads вместо передачи через postMessage (structured clone) или SharedArrayBuffer.
- Не учитывать, что TypeScript типы стираются в runtime — JSON.parse() возвращает any, runtime-валидация обязательна для внешних данных.
- Полагаться на порядок свойств объекта — в V8 он обычно стабилен, но спецификация не гарантирует для нецелочисленных ключей.
- Не профилировать GC паузы в production — clinic.js, --inspect флаг и Chrome DevTools Memory Profiler.
- Игнорировать --max-old-space-size при деплое в контейнер — по умолчанию V8 берёт 25% RAM хоста, не контейнера.
What hurts your answer
- Знать термины JavaScript, но не понимать связи между абстракциями
- Объяснять поведение через отдельные примеры вместо причинной модели
- Не связывать mental model с диагностикой ошибок
What they're listening for
- Понимает ключевые абстракции JavaScript
- Может предсказывать поведение системы через mental model
- Связывает модель с debugging и production decisions