JavaScriptSeniorExperience
Представьте, сервис на JavaScript стал медленнее или нестабильнее после релиза. Какие language/runtime-specific причины вы проверите?
Проверяю V8-деоптимизации (--trace-deopt), блокировку event loop (monitorEventLoopDelay), утечки памяти (heap snapshot), регрессии зависимостей и последовательные await вместо Promise.all.
Диагностика деградации производительности JavaScript-сервиса после релиза
Когда сервис замедляется или нестабильно работает именно после деплоя, стоит методично пройти по уровням: V8-рантайм, event loop, память, I/O и сам код.
1. Профиль CPU и V8 de-optimization
V8 компилирует «горячий» код через JIT (TurboFan). Если типы аргументов функции меняются (мегаморфный вызов) или функция слишком большая, V8 деоптимизирует её обратно до интерпретатора.
# Запуск с трассировкой деоптимизаций
node --trace-deopt --trace-opt app.js 2>&1 | grep "deopt"
# Профиль CPU через встроенный inspector
node --prof app.js
node --prof-process isolate-*.log > profile.txt
- Ищите функции с пометкой
DEOPT reason: wrong type— обычно причина в том, что новый код передаётnullили смешанные типы туда, где раньше были только числа. - Используйте Chrome DevTools → «Node.js dedicated DevTools» → Profiler для визуального flame graph.
2. Блокировка event loop
Синхронные операции длиннее ~10 мс блокируют весь event loop Node.js.
const { monitorEventLoopDelay } = require("node:perf_hooks");
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
setInterval(() => {
console.log("EL lag p99:", histogram.percentile(99) / 1e6, "ms");
histogram.reset();
}, 5000);
- Типичные причины: JSON.parse/stringify огромных объектов, синхронный fs (fs.readFileSync в обработчике запроса), регулярные выражения с катастрофическим backtracking (ReDoS).
- Инструмент:
clinic doctorиз пакета@clinic/doctorавтоматически выявляет event loop lag.
3. Утечки памяти и GC-паузы
# Heap snapshot
node --inspect app.js
# В Chrome DevTools: Memory → Take heap snapshot, сравнить два снапшота
- Новые глобальные Map/Set/массивы, которые растут без очистки — частая причина после рефакторинга кэша.
- Замыкания с большим лексическим окружением, удерживаемые в очереди событий или таймерах.
- Неотписанные event listeners:
emitter.listenerCount('data')— если растёт с каждым запросом, это утечка.
// Проверка через process.memoryUsage()
setInterval(() => {
const { heapUsed, heapTotal, external } = process.memoryUsage();
console.log({ heapUsed: Math.round(heapUsed / 1024 / 1024) + " MB" });
}, 10_000);
4. Регрессии зависимостей
- Обновление minor-версии npm-пакета может изменить внутреннее поведение. Проверьте
npm diff <pkg>@old <pkg>@newили ищите в CHANGELOG. - Транзитивные обновления:
npm ls <package>покажет, кто принёс новую версию. - Бандл-регрессии во фронтенде:
npx source-map-explorer dist/main.jsили Webpack Bundle Analyzer — ищите внезапно добавленные тяжёлые модули.
5. Async/await и Promise misuse
// Было: параллельно
const [a, b] = await Promise.all([fetchA(), fetchB()]);
// Стало (регрессия): последовательно
const a = await fetchA();
const b = await fetchB(); // ждёт a — вдвое медленнее
- Unhandled promise rejection в новых путях кода: Node.js 18+ завершает процесс по умолчанию — нестабильность может быть крашами.
- Promise leaks: промис создан, но
.catch()не добавлен и ошибка поглощается молча.
6. Worker Threads и cluster
- Если добавили CPU-intensive задачи в main thread вместо Worker — event loop деградирует.
worker.postMessage()копирует данные (structured clone); для больших буферов использоватьSharedArrayBufferилиTransferable.
Подводные камни
- V8 hidden classes: добавление свойства к объекту вне конструктора меняет hidden class и замедляет доступ к полям — частая причина деоптимизации после рефакторинга моделей.
- clinic.js:
clinic flameработает через perf_events, требует--allow-perfили запуска от root; на контейнерах это часто не настроено. - GC stop-the-world: Node.js major GC может вызывать лаги 50-200 мс — монитор event loop покажет спайки, но причина будет не в коде, а в объёме живых объектов.
- ReDoS: регулярное выражение
/(a+)+$/на входных данных с сервера — O(2^n); добавить в CI инструмент типаsafe-regex. - Синхронный dns.lookup: встроенный
http.requestиспользует синхронный libuv threadpool DNS по умолчанию; при исчерпании пула (4 потока) запросы встают в очередь. - Неправильный --max-old-space-size: увеличение лимита отсрочивает GC и маскирует утечку вместо того, чтобы её исправить.
- Разные версии Node: prod на Node 20, dev на Node 22 — поведение V8 TurboFan может отличаться, де-оптимизации воспроизводятся по-разному.
What hurts your answer
- Сразу обвинять JavaScript, не проверив соседние слои системы
- Чинить симптом без минимального воспроизведения и evidence
- Не учитывать версии, конфигурацию, окружение и recent changes
What they're listening for
- Умеет локализовать проблему вокруг JavaScript
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения