Node.JS [ru] | Серверный JavaScript
11.2K subscribers
171 photos
7 videos
25 files
3.58K links
Node.js, backend и AI-разработка: статьи, новости, вайбкодинг, автоматизация и работа с ИИ-агентами.

Личный блог автора - @just_genych
По вопросам рекламы или разработки: @g_abashkin

РКН: https://vk.cc/cJPGOP
Download Telegram
Почему «удалёнка» съедает ROI компании: расчёты потерь

В агентстве «Найт Стрит» считают, что затраты на удалёнку обычно считают по очевидной экономии: не нужен большой офис, падают расходы на командировки, нанимать можно по всей стране. Но у распределённой работы есть вторая колонка расходов, которая почти не попадает в расчёты бухгалтера. Сюда идёт лишний созвон там, где хватило бы сообщения, согласование на два дня вместо десяти минут общения в переговорке, новичок, который вникает в проект месяц, а не положенные две недели.

По отдельности это мелочи, но в команде из сотни дорогих специалистов все эти мелочи складываются в миллионы потерянной выручки за год. Цифры в статье модельные: смысл не в том, чтобы поверить процентам, а в том, чтобы подставить в формулы свои данные.

Читать далее
👎10😁5
WeakRef и циклы: тихая утечка памяти в долгоживущих процессах

WeakRef в Node.js — инструмент для умных кэшей и подписок, но в долгоживущих серверных процессах он может стать причиной неочевидной утечки памяти. Самая распространенная ошибка — циклическая структура, где WeakRef замыкается с сильными ссылками, и GC молча перестает собирать объекты.

Как возникает утечка
В серверных приложениях часто используется дерево компонентов. Если дочерний элемент хранит WeakRef на родителя, а родитель — сильную ссылку на дочерний, цикл замкнут. GC видит, что объект родителя доступен через WeakRef, но сам WeakRef жив, и сборка блокируется. Результат — плавный рост heap, незаметный неделями.

Диагностика в production
- Снимайте heap snapshots через heapdump или Chrome DevTools. Ищите раздел System / WeakRef — там лежат недоступные объекты.
- Включайте --trace-gc и --trace-gc-verbose. Они покажут, какие объекты GC не может собрать из-за слабых ссылок.
- Мониторинг process.memoryUsage() и v8.getHeapStatistics() — кривая роста без видимых причин говорит о проблеме.

Практические решения
Самый надежный способ — разорвать цикл. Используйте FinalizationRegistry для явного освобождения ресурсов:

const registry = new FinalizationRegistry((held) => {
console.log('cleaned', held);
});
class Child {
constructor(parent) {
this.parentWeakRef = new WeakRef(parent);
registry.register(this, 'child');
}
getParent() {
return this.parentWeakRef.deref();
}
}


Ошибка, которую все допускают
WeakRef внутри замыкания на this — классика. Например, в WebSocket подписках: () => this.parent.someMethod сохраняет контекст с сильной ссылкой на весь граф. Всегда пишите явную проверку: this.parentWeakRef?.deref()?.someMethod. Иначе цикл восстановится.

Вывод: WeakRef — не панацея: в долгоживущих процессах всегда проверяйте цепочки замыканий и снимайте heap snapshots, иначе тихая утечка памяти станет причиной падения сервера.
1
Диагностика и устранение indefinite suspension при мониторинге HTTP-запросов через async-hooks

Ситуация знакомая: используешь async-hooks для отслеживания длинных HTTP-запросов, и вдруг приложение начинает подвисать. Чаще всего это indefinite suspension из-за unhandled rejection.

В чём соль. async-hooks создаёт контекст для каждой асинхронной операции. Если внутри этого контекста Promise упал (rejection), а обработчика нет, хук может зависнуть в ожидании. Сборщик мусора не срабатывает, ресурсы текут — и приложение виснет.

Как ловить
Первое — глобальный перехват unhandledRejection:
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

Второе — запускай с флагом --trace-warnings, чтобы видеть стек.

Что делать
Вариант 1. Просто и надёжно — добавить .catch() в каждый Promise внутри хука:
const hook = createHook({
init(asyncId, type) {
if (type === 'HTTPINCOMINGMESSAGE') {
const requestPromise = someAsyncOperation();
requestPromise.catch(err => console.error(Request ${asyncId} failed:, err));
}
}
});
hook.enable();


Вариант 2. Явное управление контекстами через destroy. Заводишь Map, чистишь её при завершении:
const contexts = new Map();
hook.init(asyncId, type) {
if (type === 'HTTPINCOMINGMESSAGE') {
contexts.set(asyncId, Date.now());
}
}
hook.destroy(asyncId) {
contexts.delete(asyncId);
}


Вариант 3. Принудительный таймаут. Запускаешь интервал, проверяешь застрявшие контексты (скажем, те, что висят дольше 30 секунд) и прибиваешь их.

На заметку. Если Node.js 14+, бери AsyncLocalStorage — он сам управляет контекстом. И никогда не создавай Promise внутри init без .catch(). Тестируй с --async-stack-traces.

Вывод: Indefinite suspension лечится связкой глобального обработчика ошибок и чёткого контроля жизненного цикла контекстов.
1
Код я написал за две недели. Продавать его боюсь до сих пор

Три недели назад расширение прошло ревью и появилось в Chrome Web Store. С тех пор ни анонса, ни поста, ни сообщения людям, которые его ждали. Тишина. Это уже не первый раз: в мае автор точно так же на месяц пропал в код, лишь бы не заниматься продвижением. В статье разбирается, почему разработчик прячется от собственного продукта, и приводятся замеры, что происходит с цифрами, когда прятаться перестаёшь.

Читать далее →
3
Диагностика и устранение гонок данных при конкурентном доступе к node:sqlite из worker_threads через in-memory WAL-режим

В production сценариях с worker_threads и in-memory SQLite в WAL режиме разработчики часто забывают синхронизировать доступ. Итог — SQLITE_BUSY, рассинхрон данных и дубли в уникальных полях.

Проблема: гонка без синхронизации
Несколько воркеров читают и пишут в одну in-memory базу без внешней координации. WAL помогает с параллельным чтением, но не спасает от гонок записи. Типичные проявления: ошибки SQLITE_BUSY, частичные обновления и потеря данных.

Решение: один писатель с явными блокировками
Выделите один воркер для критических записей (например, инкременты). Координируйте через SharedArrayBuffer и Atomics — это дешевле мьютексов. Воркеры-читатели используют отдельные соединения в WAL.

Production-oriented пример с BEGIN IMMEDIATE
Начиная с Node.js 23 используйте DatabaseSync. Обязательно PRAGMA journal_mode=WAL; и PRAGMA synchronous=NORMAL;:

import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:', { readwrite: true, create: true });
db.exec('PRAGMA journal_mode=WAL;');
db.exec('PRAGMA synchronous=NORMAL;');

function safeUpdate(sql) {
db.exec('BEGIN IMMEDIATE;');
try {
db.exec(sql);
db.exec('COMMIT;');
} catch {
db.exec('ROLLBACK;');
throw new Error('Write failed');
}
}


BEGIN IMMEDIATE блокирует запись сразу, избегая гонок. Без него два воркера могут начать транзакции одновременно — ловите SQLITE_BUSY.

Предупреждение: in-memory WAL не для продакшена
In-memory база теряет данные при краше. Для реальных проектов используйте файловую базу с WAL — это гарантирует персистентность.

Вывод: гонки данных в SQLite лечатся одним писателем и явными блокировками через BEGIN IMMEDIATE, давая скорость WAL без потери консистентности.
👍1
Скрытый убийца производительности: синхронный DNS в dns.lookup под нагрузкой

Высоконагруженный HTTP-клиент на Node.js внезапно теряет пропускную способность? Event loop залипает при большом количестве параллельных запросов? Чаще всего проблема не в HTTP, а в том, как система разрешает доменные имена.

В чем суть?

dns.lookup (используется http.request, https, axios по умолчанию) при вызове без флагов обращается к системному getaddrinfo через libuv. Этот вызов синхронный и блокирует event loop на время резолвинга. При 1000+ RPS latency одного разрешения (например, 50 мс) превращается в 50 000 мс общего блокирования цикла.

Как диагностировать?

1. Подключить dns.promises и выполнить dns.promises.lookup при нагрузке. Если event loop задерживает микротаски, виновник найден.
2. Использовать process.hrtime.bigint() до и после dns.lookup - разница > 10 мс системного времени указывает на проблему.
3. clinic doctor - на графике async latency видны ступеньки, совпадающие с пиками DNS-запросов.

Как устранить?

Для http(s) агентов - замена на асинхронный DNS:
const { createConnection } = require('net');
const dns = require('dns/promises');
const agent = new http.Agent({
createConnection: async (options, cb) => {
const { address } = await dns.lookup(options.hostname);
const socket = createConnection({ ...options, host: address });
cb(null, socket);
},
});


Типичная ошибка: использование только maxSockets или scheduling: 'lifo' снижает частоту новых соединений, но не решает первопричину при множестве уникальных хостов. Проще - библиотеки dns-cache или cacheable-lookup.

Вывод: блокировка из-за dns.lookup - классическая проблема одного потока Node.js на стыке системных вызовов, решается заменой на асинхронное разрешение через dns.promises или кастомный агент.
Диагностика и устранение неявной фрагментации кучи в Node.js

Long-lived HTTP серверы с разными по размеру payload часто страдают от "дырявой" кучи, хотя утечек памяти нет. Память фрагментируется из-за чередования маленьких и больших Buffer-ов, что ведет к росту RSS и частым GC паузам.

Как проявляется фрагментация
После обработки HTTP запросов создаются Buffer-ы разного размера: маленькие для заголовков, большие для тела. GC освобождает их неупорядоченно, оставляя пустоты между выделенными блоками. Со временем это увеличивает RSS на 20-40% без утечек. Симптомы: падение производительности через 12+' часов работы, частые stop-the-world паузы GC.

Диагностика в production
Первый шаг — трассировка с --trace-gc. Запустите: NODE_OPTIONS="--trace-gc" node app.js. В выводе ищите Mark-sweep паузы >50ms. Второй шаг — сравнение heapdump-ов через час и сутки работы в Chrome DevTools. Ищите фрагментированные объекты Buffer, разбросанные по куче. Третий шаг — проверьте флаги V8: node --v8-options | grep -i "heap.*frag".

Решение: Buffer Pool для однородности
Создайте пул Buffer-ов фиксированного сегмента, например 1024 байта. Это снижает разнообразие размеров и уменьшает фрагментацию. Пример:

class BufferPool {
constructor(size = 1024) {
this.pool = [];
this.size = size;
}
alloc(size) {
if (size > this.size) return Buffer.allocUnsafe(size);
if (this.pool.length) return this.pool.pop().slice(0, size);
return Buffer.allocUnsafe(this.size);
}
free(buf) { this.pool.push(buf); }
}


Практические советы и ошибки
- Для максимального снижения фрагментации используйте Buffer.allocUnsafeSlow вместо Buffer.allocUnsafe в пуле — он избегает shared memory pool.
- Настройте --max-old-space-size и --optimize-for-size для ограничения кучи.
- Типичная ошибка — использовать Buffer.allocUnsafe для всех случаев без разбора. Это увеличивает фрагментацию из-за случайных размеров.
- В Node 20+ можно изолировать тяжелые операции через --experimental-vm-modules в отдельных контекстах.

Вывод: Фрагментация кучи в long-lived серверах — реальная проблема, решаемая через Buffer Pool с фиксированным сегментом и контроль аллокаций, а не через поиск утечек.
Неявный deadlock в Node.js: AsyncLocalStorage + worker_threads + await в run()

Многие знают AsyncLocalStorage для проброса контекста, но скрещивание его с пулом worker_threads и await внутри колбэка run() создаёт неочевидный deadlock. Эта ошибка часто остаётся незамеченной до первого production-инцидента, когда пул потоков залипает при высокой нагрузке.

Как проявляется deadlock
Внутри run() вы помещаете ID запроса и вызываете await, пока воркер не закончит задачу. Если потоков в пуле ограниченное число (например, 2), а запросов много, каждый await блокирует run(), не освобождая поток из пула. Воркеры заняты, потоки ждут await — контекст AsyncLocalStorage не отпускается до полного завершения колбэка.

Пример problem:
const als = new AsyncLocalStorage();
async function processRequest(data) {
await als.run({ requestId: Date.now() }, async () => {
const worker = new Worker('./worker.js');
const result = await new Promise((resolve, reject) => {
worker.on('message', resolve);
worker.on('error', reject);
});
console.log(als.getStore().requestId);
});
}


Как обнаружить
- Залогируйте время выполнения воркеров: рост с увеличением числа запросов — тревожный сигнал.
- Используйте clinic.js или 0x: они покажут аномальные пики ожидания.
- Типичная ошибка: пул из 2 воркеров и 100 параллельных запросов — все зависнут через несколько секунд.

Практический совет
Не оставляйте await внутри run(). Вынесите ожидание наружу:
async function processRequest(data) {
const worker = new Worker('./worker.js');
const result = await new Promise(/*...*/);
als.run({ requestId: Date.now() }, () => {
console.log(als.getStore().requestId);
});
}

Либо передавайте контекст через worker.postMessage() — это проще и исключает deadlock.

Предупреждение
AsyncLocalStorage отлично работает на веб-серверах с краткими синхронными операциями, но await внутри run() с ограниченным пулом потоков — прямой путь к deadlock. Проверяйте такие места на этапе code review.

Вывод: Избегайте await внутри колбэка AsyncLocalStorage.run() при работе с пулом worker_threads — выносите асинхронное ожидание наружу или передавайте контекст через сообщения.
Как я хакнул рынок труда: пишем свой ИИ-комбайн для автооткликов на HH.ru

Если вы хоть раз искали работу в IT за последний год, то знаете, что рынок беспощаден к новичкам. Нужно откликнуться на сотни вакансий, а в итоге получаешь отказы от роботов. Чтобы пробиться через фильтры HR, нужно под каждую вакансию писать уникальное сопроводительное письмо. В этой статье автор сделал полный разбор того, как написал собственного автономного ИИ-агента, который ищет вакансии, фильтрует мусор с помощью локальной нейросети, пишет персонализированные сопроводительные письма и отчитывается в Telegram, пока пользователь спокойно занимается своими делами. Скрипт задумывался бесплатным, автономным и не требующим настройки вокруг платных API.

Читать далее
👎3
Buffer.write со str.length — тихая бомба под production.

Ты пишешь в буфер, передаёшь offset через str.length, и данные записываются в неверное место. Всё молча. Никаких ошибок. Просто битая информация.

В UTF-8 один символ может занимать от 1 до 4 байт. «Привет» — это 6 символов, но 12 байт. Если offset посчитан по длине строки, ты фактически говоришь Buffer: «запиши мне данные со смещением 6 байт». А нужно было с 12. Разница в два раза.

Я такое видел в production: кусок бинарного заголовка перезаписывался, файл читался как мусор. И никто не мог понять, почему.

Диагностика
— используй Buffer.byteLength(str, 'utf8') для вычисления реального размера в байтах;
— сравнивай str.length и Buffer.byteLength(str) — если различаются, в строке есть не-ASCII символы;
— проверяй логи, где размер буфера и смещение не сходятся.

Исправление
Простое решение:
const byteOffset = Buffer.byteLength(str, 'utf8');
buf.write(str, byteOffset);

Или вообще запиши с начала: buf.write(str, 0, 'utf8').

Типичная ошибка
— расчёт offset вручную через str.length * someCoefficient — это антипаттерн;
— молчаливое переполнение буфера без проверки остаточного места;
— путаница между символьным и байтовым смещением в одном бинарном сериализаторе.

Профилактика
— всегда считай байты через Buffer.byteLength();
— не вычисляй offset вручную для строк с Unicode;
— в TypeScript включай strict — он ловит часть таких косяков.

Вывод:
Silent data corruption — гадость редкая, но реальная. Проверь код: не используешь ли str.length как offset для Buffer.write?
1
PROvoke на Analyst Days: как мы выступали в корсете и что из этого вышло

Привет, Хабр! В первой части я рассказывал, как мы с Татьяной Маркиной готовили доклад «Провоок» на Analyst Days — про споры, студию в Краснодаре, логистический ад и идею с кнутом и пряником. А сегодня — вторая часть: что произошло на сцене, какие роли мы играли, почему корсет так и не вышел в свет, и как один разговор в кулуарах перевернул моё понимание публичных выступлений. Подробности — во второй части статьи.

Источник: Анатомия парного доклада: как мы собирали «PROvoke» на Analyst Days. Часть 2 — выступление и фидбек
Как один отказ бизнесу сэкономил компании 10 миллионов рублей

К концу проекта внедрения нового биллинга бизнес предложил перенести архивные продукты и исторические данные по ним. Архивные продукты использовались редко — для разбора спорных ситуаций с клиентами. Изначально было решено не избавляться от старого биллинга, а использовать его для чтения ретроданных. После оценки работ выяснилось, что типового механизма нет — нужно менять архитектуру, разрабатывать сценарии миграции, тестировать и поддерживать. Оценка составила около 10 миллионов рублей. Пересчет финансовой модели показал, что дополнительные инвестиции увеличивают срок окупаемости проекта на один год. Компания вкладывала бы деньги в функциональность без измеримой бизнес-ценности. Было принято решение отказать бизнесу и предложить оставить эти данные доступными для чтения в legacy биллинге.

Источник
Гео-аналитическая платформа вдвоём за 2,5 месяца: история spec-driven разработки

Автор поделился опытом старта проекта с чистого листа. Перед ним стояла амбициозная задача, не было легаси и была полная свобода выбора подхода. Он решил максимально довериться AI и больше не трогать код. Изначально были сомнения, что такой подход выдержит реальный масштаб, так как опыт подсказывал, что чем больше проект, тем быстрее AI путается в контексте и упирается в лимиты.

Через 2,5 месяца работы вдвоём удалось запустить гео-аналитическую платформу. Автор отмечает, что в до-AI эпоху такой проект строили бы годами. Этот опыт изменил его представление о разработке.

Читать далее
Маск внезапно выкатил Grok 4.5 — первую версию, заточенную под код и агентов 😨

Обучали её в связке с Cursor, и по бенчмаркам она уже перепрыгивает GPT-5.5, заодно держится на уровне Opus 4.8. Terminal-Bench: у неё 83%, а у Opus 4.8 — 78,9%.

Прайсинг: $2/$6 за миллион токенов. «Opus-уровень, но шустрее, меньше жрет токенов и дешевле» — написал Маск в X. В Cursor она уже есть на всех тарифах. Завтра ждем ответочку от Альтмана 😊

👉 Node.JS [ru]
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
Грейсфул шатдаун: как SIGTERM и postMessage ломают порядок сигналов

Каждый middle/senior разработчик Node.js сталкивался с грейсфул шатдауном, но редкий замечает скрытую ловушку — смешивание process.on('SIGTERM') и Worker.prototype.postMessage. Из-за разницы приоритетов событий и микрозадач теряются данные в production.

Проблема приоритетов событий
SIGTERM — системное событие с высоким приоритетом в цикле событий. postMessage — асинхронная операция, попадающая в очередь микрозадач. Если SIGTERM приходит до завершения отправки сообщения, оно обрабатывается уже после shutdown. Результат — потеря данных, которые должны были уйти в Worker.

Production-пример и диагностика
Рассмотрим типичный код:
process.on('SIGTERM', () => {
console.log('Shutdown');
worker.terminate();
});
worker.postMessage({ type: 'data', payload: result });


Здесь postMessage может быть прерван сигналом. Как диагностировать:
* Логируйте порядок событий с таймстемпами.
* Используйте process.on('SIGTERM', { once: true }) для однократного вызова.
* Проверяйте флаг isShuttingDown перед любым postMessage.

Практическое исправление
Решение — флаги состояния:
let isShuttingDown = false;
process.on('SIGTERM', () => { isShuttingDown = true; });
if (!isShuttingDown) worker.postMessage({ ... });


Для надёжности — SharedArrayBuffer для атомарной синхронизации между основным потоком и Worker, если требуется строгий контроль.

Типичная ошибка
Многие путают: проблема не в доступе к process из Worker threads — Worker его не имеет. Корень — порядок микрозадач в основном потоке, где postMessage может быть отложен после обработки SIGTERM.

Вывод: Используйте флаги состояния перед асинхронными операциями в обработчиках сигналов — это предотвращает потерю данных при грейсфул шатдауне.
👍21
This media is not supported in your browser
VIEW IN TELEGRAM
Claude Code теперь стерильно для стаи

Наваливаться на одну сессию в терминале можно гуртом. Плюс теперь разрешено стыковать свои инстансы Claude, чтоб они балакали между собой.

Потыкать

👉 Node.JS [ru]
Please open Telegram to view this post
VIEW IN TELEGRAM
2
OpenAI наконец-то почесались и закатили апдейт: GPT-5.6 Sol, Terra и Luna — уже в проде, дружок. Постепенно раздают ранний доступ по всему шарику.

Вот пруф, если глазам не веришь

👉 Node.JS [ru]
Please open Telegram to view this post
VIEW IN TELEGRAM
Anthropic раскатал схему работы Fable 5 в обнимку с Sonnet 5 — тут два варианта

Первый вариант — Fable 5 втыкается как «советник» (Advisor). Sonnet 5 тащит задачу, но периодически дёргает Fable 5 за стратегическим советом. На тесте SWE-bench Pro такая связка вывозит примерно 92% от качества Fable 5, при этом платишь всего 63% от её цены. Fable 5 дёргают примерно раз на задачу. Подробнее в документации.

Второй расклад — Fable 5 работает оркестратором. Она разруливает план и раздаёт таски исполнителям — Sonnet 5. В Benchmark-тестах BrowseComp на Claude Managed Agents такая конфигурация показала 96% производительности Fable 5, а стоила 46% от оригинала. Жирные по ресурсам задачи скидывают на Sonnet 5. Пример в кукбуке.

Claude Managed Agents переваривает оба подхода (документация). Каждый подагент держит свой кэш, так что при повторных вызовах не придётся платить за полную пережевку контекста.

👉 Node.JS [ru]
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM