Настоящий JavaScript
Открыть в Telegram
Тот самый канал по JavaScript. Личный блог автора - @just_genych По вопросам рекламы или разработки: @g_abashkin
Больше6 221
Подписчики
-424 часа
-287 дней
-1030 день
Загрузка данных...
Похожие каналы
Облако тегов
Входящие и исходящие упоминания
---
---
---
---
---
---
Привлечение подписчиков
июнь '26
июнь '26
+105
в 0 каналах
май '26
+384
в 0 каналах
Get PRO
апрель '26
+83
в 0 каналах
Get PRO
март '26
+71
в 0 каналах
Get PRO
февраль '26
+23
в 0 каналах
Get PRO
январь '26
+18
в 0 каналах
Get PRO
декабрь '25
+13
в 0 каналах
Get PRO
ноябрь '25
+24
в 0 каналах
Get PRO
октябрь '25
+22
в 0 каналах
Get PRO
сентябрь '25
+21
в 0 каналах
Get PRO
август '25
+20
в 0 каналах
Get PRO
июль '25
+38
в 0 каналах
Get PRO
июнь '25
+52
в 0 каналах
Get PRO
май '25
+121
в 0 каналах
Get PRO
апрель '25
+88
в 0 каналах
Get PRO
март '25
+363
в 20 каналах
Get PRO
февраль '25
+30
в 0 каналах
Get PRO
январь '25
+38
в 0 каналах
Get PRO
декабрь '24
+41
в 0 каналах
Get PRO
ноябрь '24
+64
в 2 каналах
Get PRO
октябрь '24
+70
в 1 каналах
Get PRO
сентябрь '24
+84
в 2 каналах
Get PRO
август '24
+70
в 0 каналах
Get PRO
июль '24
+115
в 0 каналах
Get PRO
июнь '24
+89
в 4 каналах
Get PRO
май '24
+101
в 0 каналах
Get PRO
апрель '24
+121
в 0 каналах
Get PRO
март '24
+132
в 0 каналах
Get PRO
февраль '24
+101
в 0 каналах
Get PRO
январь '24
+86
в 0 каналах
Get PRO
декабрь '23
+90
в 0 каналах
Get PRO
ноябрь '23
+16
в 0 каналах
Get PRO
октябрь '23
+15
в 0 каналах
Get PRO
сентябрь '23
+16
в 0 каналах
Get PRO
август '23
+24
в 0 каналах
Get PRO
июль '23
+35
в 0 каналах
Get PRO
июнь '23
+20
в 0 каналах
Get PRO
май '23
+15
в 0 каналах
Get PRO
апрель '23
+264
в 0 каналах
Get PRO
март '23
+17
в 0 каналах
Get PRO
февраль '23
+207
в 0 каналах
Get PRO
январь '23
+40
в 0 каналах
Get PRO
декабрь '22
+478
в 0 каналах
Get PRO
ноябрь '22
+1 627
в 0 каналах
Get PRO
октябрь '22
+45
в 0 каналах
Get PRO
сентябрь '22
+451
в 0 каналах
Get PRO
август '22
+459
в 0 каналах
Get PRO
июль '22
+1 991
в 0 каналах
Get PRO
июнь '22
+1 567
в 0 каналах
Get PRO
май '22
+1 769
в 0 каналах
Get PRO
апрель '22
+97
в 0 каналах
Get PRO
март '22
+2 701
в 0 каналах
| Дата | Привлечение подписчиков | Упоминания | Каналы | |
| 27 июня | 0 | |||
| 26 июня | 0 | |||
| 25 июня | 0 | |||
| 24 июня | +1 | |||
| 23 июня | 0 | |||
| 22 июня | +1 | |||
| 21 июня | 0 | |||
| 20 июня | 0 | |||
| 19 июня | 0 | |||
| 18 июня | 0 | |||
| 17 июня | 0 | |||
| 16 июня | +1 | |||
| 15 июня | +1 | |||
| 14 июня | +2 | |||
| 13 июня | +2 | |||
| 12 июня | +11 | |||
| 11 июня | +39 | |||
| 10 июня | +24 | |||
| 09 июня | +17 | |||
| 08 июня | 0 | |||
| 07 июня | 0 | |||
| 06 июня | 0 | |||
| 05 июня | +2 | |||
| 04 июня | 0 | |||
| 03 июня | 0 | |||
| 02 июня | +2 | |||
| 01 июня | +2 |
Посты канала
Кеш для Intl — не опция, а необходимость при heavy load интернационализации
Тема performance в интернационализации часто недооценивается. В production — будь то SPA с динамической сменой локали, Node.js API, отдающий отформатированные цены тысячам пользователей, или SSR с рендерингом сотен дат на страницу — каждый вызов
new Intl.DateTimeFormat() или Intl.NumberFormat() без кеша нагружает процессор парсингом CLDR-данных и построением форматтера. Типичная ошибка: писать форматтинг прямо внутри рендера или цикла, думая, что это "микрооптимизация".
Кешируйте форматтеры, не конструкторы
Создавать форматтер на каждый вызов — путь к тормозам. Решение: храните готовые экземпляры в Map или WeakMap, ключом делайте строку JSON.stringify({ locale, ...options }). Но будьте внимательны: если опции меняются часто (например, динамический timeZone), кеш может не дать выгоды. Всегда включайте изменчивые параметры в ключ.
Типизируйте опции явно, а не строковыми literals
В TypeScript DateTimeFormatOptions — это широкий тип, легко передать 'number' вместо 'numeric'. Ошибка всплывет в runtime. Создайте строгий union:
type DatePart = 'numeric' | '2-digit';
type StrictOptions = {
year?: DatePart;
month?: DatePart;
day?: DatePart;
};
Это ловит опечатки на этапе компиляции. Практический совет: для Intl.NumberFormat используйте NumberFormatOptions с кастомными константами валют — это предотвращает случайное переключение формата.
Предварительно проверяйте кастомные локали
Intl.DateTimeFormat.supportedLocalesOf() — не просто метод, а must-have перед вызовом конструктора. Без него передача неподдерживаемой локали генерирует RangeError. Для Intl.NumberFormat кешируйте по ключу locale:currency: когда пользователь переключает язык на лету, это предотвращает фризы в UI. В Node.js проверьте, не используете ли вы small-icu — без full ICU кастомные локали приведут к багам за день до релиза.
Не считайте кеширование Intl микрооптимизацией. Это базовый паттерн для надежного DX в heavy-load системах.
Вывод: Всегда кешируйте экземпляры Intl.XXXFormat по стабильному ключу и типизируйте опции — это дает стабильную производительность и предотвращает runtime-ошибки в production.| 2 | Вложенные Promise-конструкторы и скрытые утечки памяти: почему new Promise(resolve => executor(resolve)) опаснее, чем кажется, и как типизировать ручное управление resolve/reject
На code review я часто вижу этот паттерн от middle и senior разработчиков. В production — в Node.js сервисах, подписках на EventEmitter, SPA с кастомными колбэками — это фабрика висячих промисов, которые никогда не завершаются. Основная ошибка: передача resolve как колбэка без обёртки.
Почему промис «не зарезолвится»?
Когда вы пишете new Promise(resolve => executor(resolve)), внутренний executor сохраняет ссылку на resolve как колбэк. Если executor — подписка на EventEmitter или долгоживущий объект, resolve живёт в замыкании, пока жив объект. Промис ждёт вызова, которого не будет — утечка памяти гарантирована. GC не соберёт его из-за замкнутой ссылки.
Пример:
function subscribeToEvents(emitter) {
return new Promise(resolve => {
emitter.on('data', resolve); // resolve висит, пока emitter жив
// emitter никогда не эмитит — промис висит вечно
});
}
Типизировать resolve/reject: не просто (value?) => void
Простая сигнатура не показывает, что resolve должен вызываться ровно один раз. Правильнее — замыкание с внутренним состоянием или явный Deferred.
Лучшее решение — Promise.withResolvers() (ES2024) или кастомный Deferred:
interface Deferred<T> {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}
function createDeferred<T>(): Deferred<T> {
let resolve!: Deferred<T>['resolve'];
let reject!: Deferred<T>['reject'];
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
Deferred даёт явное управление жизнью промиса — вы контролируете, когда и как его завершить. Это trade-off: больше кода, но меньше утечек.
Как избежать висячих промисов
Совет: если сохраняете resolve в объекте (подписка на класс), удаляйте ссылку после завершения через .finally(). Иначе накопите кучу функций, которые GC не вычистит.
const deferred = createDeferred<Data>();
emitter.on('data', deferred.resolve);
deferred.promise.finally(() => {
emitter.off('data', deferred.resolve);
});
Предупреждение: передавать resolve как колбэк в сторонние подписки без привязки к жизненному циклу — плохая идея. Используйте Deferred с гарантией однократного вызова и очисткой. Утечки через промисы трудно ловить, но код должен быть чистым по памяти.
Вывод: Ручное управление resolve/reject в JavaScript требует явного контроля жизненного цикла — без Deferred или Promise.withResolvers() вы рискуете утечками памяти, которые не видны в тестах, но проявляются в production при долгоживущих подписках. | 113 |
| 3 | Buffer и Uint8Array: грабли, которые стоят часов дебага
Одна из самых коварных ловушек в Node.js — смешивание Buffer и Uint8Array при парсинге бинарных протоколов. На первый взгляд они взаимозаменяемы: Buffer наследует от Uint8Array. Но под капотом — разные контракты, и "тихая" конвертация данных может убить zero-copy или вызвать TypeError.
Проблема: slice vs subarray
Допустим, ты парсишь TCP-поток с zero-copy, работая с подмассивами:
// Ошибка: buffer.slice() копирует данные в Node <20
function parsePacket(raw: Buffer): number {
const header = raw.slice(0, 4);
return header.readUInt32BE(); // работает, но копия
}
// Правильно: subarray() делает zero-copy view
function parsePacket(raw: Buffer): number {
const header = raw.subarray(0, 4); // Uint8Array
// TypeError: header.readUInt32BE is not a function
}
Ключевые грабли
* Buffer.prototype.slice() до Node 20 копирует данные, убивая zero-copy.
* Buffer.prototype.subarray() возвращает Uint8Array, а не Buffer. Методы .readUInt* недоступны.
* Обратная конвертация (передача Uint8Array в Buffer API) — тихая, но с сюрпризом: методы Buffer не работают.
Правильный подход: DataView
Для кросс-платформенного zero-copy парсинга используй DataView поверх ArrayBuffer:
class PacketParser {
private view: DataView;
constructor(private buffer: ArrayBuffer | Buffer) {
this.view = new DataView(
buffer instanceof Buffer ? buffer.buffer : buffer
);
}
readUint32(offset: number): number {
return this.view.getUint32(offset, false); // big-endian
}
readSlice(offset: number, length: number): ArrayBuffer {
return this.buffer.slice(offset, offset + length);
}
}
// Zero-copy: subarray, затем DataView
const raw = Buffer.from([0, 0, 0, 42]);
const firstPacket = raw.subarray(0, 4); // Uint8Array
const parser = new PacketParser(firstPacket.buffer);
console.log(parser.readUint32(0)); // 42
Практические советы
* Не используй Buffer.read* при zero-copy — они не работают с Uint8Array.
* Для кросс-платформенного парсинга (браузер + Node) — только DataView.
* Buffer.concat возвращает Buffer и копирует данные — явная конвертация.
* Если нужен гибридный парсер, оберни все в new Uint8Array(bufferOrView) и используй TextDecoder или DataView.
Предупреждение
Неявное смешивание типов — главный источник багов производительности и логики. Zero-copy теряется, как только ты вызываешь .slice() или передаешь Buffer в API браузера (например, WebSocket.send).
Вывод: Для надежного zero-copy парсинга бинарных протоколов используй DataView поверх subarray — это единственный способ избежать тихих конвертаций и сохранить производительность в production. | 171 |
| 4 | Branded Memento: типизация приватного состояния без компромиссов
Классический паттерн Снимок (Memento) часто сводится к поверхностной инкапсуляции: TypeScript private ломается через as any или Object.keys, а runtime-доступ к состоянию остается открытым. В production — будь то state management в SPA, логирование в Node.js-сервисах или rollback в API-клиентах — это приводит к неявным мутациям и багам. Решение лежит в комбинации branded типов и WeakMap.
Branded type как физический барьер
Memento — не контейнер данных, а заглушка с уникальным символом:
declare const MementoBrand: unique symbol;
interface Memento {
readonly [MementoBrand]: void;
}
Снаружи невозможно прочитать [MementoBrand] — он просто не существует в runtime. Любая попытка as any не даст доступа к реальному состоянию, так как его там нет.
WeakMap: приватность без утечек
Реальное состояние хранится в WeakMap<Memento, State>. Ключ — сам объект Memento, который генерируется внутри класса:
const stateMap = new WeakMap<Memento, { balance: number }>();
class BankAccount {
private balance = 0;
createSnapshot(): Memento {
const memento = {} as Memento;
stateMap.set(memento, { balance: this.balance });
return memento;
}
restore(memento: Memento) {
const state = stateMap.get(memento);
if (state) this.balance = state.balance;
}
}
Сборщик мусора автоматически удаляет запись, когда снимок больше не используется. Никаких утечек.
Практический совет и типичная ошибка
Сериализовать такой Memento напрямую в JSON нельзя — WeakMap не итерируется. Решение: если нужна де/сериализация, замени WeakMap на Map<string, State> и генерируй UUID для снимка. Теряешь автогарбаж, но получаешь поддержку persistence.
*Типичная ошибка:* пытаться сделать Memento как простой объект с полями. Даже readonly в TS не защитит от Object.assign или деструктуризации в модульных тестах.
Вывод: Branded types в паре с WeakMap превращают TypeScript из системы типов в инструмент настоящей инкапсуляции, где приватность гарантируется на уровне исполнения, а не только компиляции. | 170 |
| 5 | 🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы
Внутри статьи она подробно расписывает этапы собеседований, лайфхаки и делится учебными ресурсами, которые ей помогли.
Плюс девушка великодушно оставила ссылки на свой Notion с полезными заметками по математике и LLM.
✖️ xCode Journal | 255 |
| 6 | Node.js: WeakRef, FinalizationRegistry и GC-триггеры для long-running процессов
Long-running процессы в Node.js — зона риска утечек памяти. ES2021 даёт два API для активного контроля над GC: WeakRef и FinalizationRegistry. Многие разработчики полагаются на автоматическое управление памятью, не учитывая, что GC не может освободить ресурсы, удерживаемые сильными ссылками, а сами API недетерминированы.
WeakRef: слабые ссылки
WeakRef не удерживает объект в памяти. GC может удалить его, если нет сильных ссылок. Это полезно для кэшей, где не требуется гарантированное хранение данных.
const cache = new Map();
function getOrCreate(key) {
const ref = cache.get(key);
if (ref) {
const obj = ref.deref();
if (obj) return obj;
cache.delete(key);
}
const obj = new ExpensiveObject();
cache.set(key, new WeakRef(obj));
return obj;
}
deref() может вернуть undefined в любой момент — GC асинхронен. Если подряд дёргаешь deref(), не жди, что вернёт то же самое. Типичная ошибка — полагаться на deref() как на гарантированный доступ.
FinalizationRegistry: колбэк после GC
Вызывается после сборки объекта. Идеален для очистки нативных ресурсов, например, закрытия файловых дескрипторов.
const registry = new FinalizationRegistry((handle) => {
cleanupNativeHandle(handle);
});
function createResource() {
const handle = openNativeConnection();
registry.register({ handle }, handle);
return { handle };
}
Предостережения: колбэк не гарантирует порядок и может не выполниться при падении процесса. В продакшене я бы не вешал на него критичную логику вроде записи в БД. Только освобождение нативных хендлов или логгирование.
Паттерн: MemoryGuard
Триггер на основе FinalizationRegistry для мониторинга памяти. Помогает выявить растущие объекты, но не заменяет ручное управление.
class MemoryGuard {
constructor(thresholdMB) {
this.threshold = thresholdMB;
this.registry = new FinalizationRegistry(() => this.onGC());
}
track(obj) { this.registry.register(obj, () => {}); }
onGC() {
const usage = process.memoryUsage().heapUsed / 1024 / 1024;
if (usage > this.threshold) console.warn(Memory spike: ${usage.toFixed(2)} MB);
}
}
По опыту, этот паттерн хорош скорее для локальной отладки, чем для прода. В production лучше комбинировать с метриками в Prometheus и лимитами heap (--max-old-space-size=4096).
Production-паттерны
* LRU-кэш с WeakRef — автоочистка при нехватке памяти, но без гарантий времени.
* async_hooks для мониторинга асинхронных ресурсов: ловит утечки через незакрытые сокеты или таймеры.
* Регулярные snapshot console.profile() для поиска растущих объектов.
* Слушать process.on('warning') для уведомлений о превышении лимитов.
Я часто вижу, как разработчики забывают про async_hooks: он ловит утечки через незакрытые сокеты или таймеры. Это дешевле, чем дебажить heap snapshot.
Вывод: WeakRef и FinalizationRegistry — полезные инструменты для оптимизации памяти, но их недетерминированность требует ручного управления ссылками (WeakMap, cancellable promises) для production-надёжности. | 227 |
| 7 | Строгая типизация event-сорсинга: как discriminated unions спасают от ошибок в сагах
В проектах с event-сорсингом и сагами магические строки для типов событий — частая причина production-багов. Одна опечатка в 'user_deleted' против 'user_deleted_at' — и рантайм ломает логику, которую TypeScript мог отловить на этапе компиляции. Senior-разработчики игнорируют это, пока не получают инцидент.
Discriminated unions для событий
Вместо строкового enum или raw string используй union-тип с литеральным полем type. TypeScript автоматически сужает payload в conditional-блоках:
type UserEvent =
| { type: 'USER_CREATED'; payload: { id: string; name: string } }
| { type: 'USER_DELETED'; payload: { id: string } };
function handle(event: UserEvent) {
if (event.type === 'USER_CREATED') {
// payload: { id: string; name: string } — без as
console.log(event.payload.name);
}
}
Ошибка: хардкодить строку в каждом обработчике. Используй discriminated union, чтобы компилятор проверял, что payload соответствует типу.
Type-safe саги без магии
В саге, которая агрегирует события, union-тип предотвращает обращение к несуществующим типам:
type SagaEvent =
| { type: 'ORDER_CREATED'; payload: { orderId: string } }
| { type: 'PAYMENT_CONFIRMED'; payload: { paymentId: string } };
function processSaga(events: SagaEvent[]) {
// Ошибка: 'ORDER_DELETED' не входит в union
// events.find(e => e.type === 'ORDER_DELETED');
}
Практический совет: при добавлении нового события расширяй union явно — это заставит обновить все саги, где это событие участвует. Так ты не пропустишь ветку в switch или pattern-matching.
Trade-off: размер union
При 15-20 типах union становится громоздким. Но надежность перевешивает: компилятор гарантирует, что ты не используешь несуществующий тип или payload. Альтернатива с магическими строками — это deferred runtime failure, который проявится в production.
Вывод: Discriminated unions в event-сорсинге — это не избыточность, а контракт, который делает невозможным целый класс ошибок, связанных с опечатками и несоответствием payload. | 225 |
| 8 | 😁 Пункта про стоимость и требуемые характеристики к железу не хватает
✖️ xCode Journal | 263 |
| 9 | import() не так безопасен, как кажется: tree-shaking, кеш и TTI под ударом
Динамический импорт — мощный инструмент ленивой загрузки, но в production он часто преподносит сюрпризы: сборщик не может его статически проанализировать, что ломает tree-shaking, кеширование и time-to-interactive. Разработчики полагаются на интуицию, забывая, что браузер и бандлер видят import() как чёрный ящик.
Tree-shaking: сборщик пасует
Webpack и Vite не рискуют удалять мёртвый код внутри динамически импортируемого модуля — деструктуризация после await import() не даёт им статических гарантий. Весь модуль летит в чанк, даже если нужна одна функция. Например, библиотека валидации с 20KB кода «съест» бандл целиком, хотя ты вызвал лишь isEmail. Единственный выход — статически выделять подмодули через гранулярные файлы.
Кеширование: динамический путь — гарантированный промах
Если имя чанка формируется runtime, скажем import(./locales/${lang}.json), браузер не использует кеш — каждый вызов генерирует новый URL. Хеши в именах файлов здесь не спасают. Фиксируй чанки через webpackChunkName или import.meta.glob в Vite, чтобы сборщик сам управлял адресацией и позволял браузеру кешировать по хешу.
TTI: скрытая блокировка после загрузки
Даже когда чанк загружен, await import() внутри интерактивного события (клик, скролл) может блокировать UI из-за синхронной инициализации — например, разбора большого JSON или создания Web Worker. DevTools покажет загрузку сети, но не скажет про 200 мс занятого потока после. Добавляй rel="prefetch" или rel="preload" для критичных модулей, чтобы сместить тяжесть на загрузку до взаимодействия.
Вывод: Динамический import() требует ручного контроля сборщика и планирования загрузки — без статических хинтов и анализа бандла вы рискуете получить раздутые чанки и провалы в производительности, которые невозможно отловить в dev-среде. | 287 |
| 10 | parseInt vs Number: когда строковая типизация подставляет в production
Каждый разработчик сталкивался с преобразованием строк в числа, но немногие осознают, насколько разные грабли скрываются за parseInt и Number. В production эта тема критична при валидации user input, парсинге API-ответов или обработке CSV. Типичная ошибка — полагаться на один метод без учета edge-кейсов, что приводит к молчаливым потерям данных или скрытым багам в финансовых, аналитических или гео-сервисах.
Разные ожидания от одного преобразования
parseInt('100px') → 100. Он игнорирует хвост, что удобно для CSS, но убивает, когда требуется строгая валидация. Number('100px') → NaN. Уже противоречие. Выбери не тот метод — и production упадет или, что хуже, обработает мусор. Запомни: Number строже, parseInt терпимее к префиксам, но оба обманывают на пустых строках.
Молчаливые нули и бесконечности
Number('') → 0. Пустая строка в JSON-валидации становится нулем, и ты считаешь то, чего нет. Number(' \t\n') → тоже 0. Особенно опасно в формах, где пробелы — частый сценарий. parseInt(Infinity) → NaN, а Number(Infinity) → Infinity. Разное поведение на одних данных, совместимое с TS-типами, но ломающее бизнес-логику.
Legacy восьмерички и запятые
В старых браузерах parseInt('010') мог вернуть 8. Сейчас стандарт parseInt требует основание 10, но если есть legacy, не расслабляйся. Еще одна ловушка: parseInt('1,000') → 1. Запятая игнорируется, и тысяча превращается в единицу. Для CSV или локализованных чисел это катастрофа. Используй Number с replace запятых или Intl.NumberFormat.
Практический совет: кастомный тип для safe parsing
В sharing types для API или валидации полей вводи явный тип:
type SafeNumber = number | 'NaN' | 'Inf'
function parseNumericInput(value: unknown): SafeNumber {
if (typeof value === 'number' && !isNaN(value)) return value
if (typeof value !== 'string') return 'NaN'
const trimmed = value.trim()
if (trimmed === '') return 'NaN'
const num = Number(trimmed)
if (isNaN(num)) return 'NaN'
if (!isFinite(num)) return 'Inf'
return num
}
Это не спасет от всех граблей, но делает неявное явным. В JSON.parse можно передать reviver, проверяющий строки "NaN" и "Infinity". Без него такие значения пройдут как строки, сломав runtime-ожидания.
Вывод: В production всегда используй кастомные утилиты с явной обработкой пустых строк, Infinity и строковых NaN, а не прямые вызовы parseInt или Number — это убережет от невидимых дефектов в числовых данных. | 282 |
| 11 | День сурка frontend-разработчика
Зарплата стоит, скучные задачи день за днем, календарь забит созвонами, которые не влияют вообще ни на что.
Откликаешься на вакансии, а в ответ тишина либо какие-то мутные конторы. На собесах вместо нормальной оценки навыков цирк с алгоритмами на скорость, как будто ты на олимпиаде, а не работу ищешь.
И самое неприятное, пока ты варишься в этом болоте, кто-то спокойно проходит собесы и уходит в Яндекс, VK или на хорошую Валютную удаленку без лишней драмы.
Есть классные проекты и сильные команды, где разработчиков действительно ценят, дают расти, поддерживают развитие и платят достойно и ты можешь туда попасть!
👋 Меня зовут Тихон, привет! Я — действующий Frontend-разработчик и ментор. Я за руку довожу до оффера на хорошую позицию в Big Tech и сопровождаю на испытательном сроке.
Также из учеников я собираю комьюнити, где уже более 220 frontend-разработчиков🫂
А в своем канале:
👉Объясняю, как проходить HR-фильтр и превращать отклики в реальные приглашения
👉Помогаю найти мотивацию, борюсь убеждениями, которые мешают развиваться
👉На примерах объясняю, как проходить собеседования, включая техничку
👉Разбираю резюме и делюсь лайфхаками, например как аккуратно “пинговать” рекрутеров
А еще регулярно публикую полезные материалы:
▪️Задачи, на которых валяться кандидаты
▪️База по микрофронтам
▪️Подборка из 100+ каналов с вакансиями для разработчиков
▪️100 вопросов, которые точно помогут тебе на собеседовании
▪️Чек лист проверки своего резюме
А еще у меня множество успешных кейсов и отзывов, найти их можно в канале.
Реклама, erid: 2W5zFJixYDf ИП Галактионов Тихон Витальевич, ИНН 771618975809 | 265 |
| 12 | CPU’s hidden bottleneck: cache misses, false sharing и Node.js event loop
Когда high-load сервис на Node.js начинает тормозить, обычно думают на I/O, GC или асинхронщину. Я часто видел другое: настоящий убийца производительности — CPU cache misses и false sharing. В production на Node.js, особенно в воркерах с SharedArrayBuffer, это критично для финансовых транзакций, игровых серверов или обработки медиа.
Что такое false sharing в контексте Node.js
Допустим, у вас массив флагов для тысячи воркеров в Worker Threads. Вы обновляете их из разных потоков.
// Плохо: флаги лежат рядом в памяти
const flags = new Int32Array(1024 * 1024);
// Воркер 1 пишет в flags[0], Воркер 2 — в flags[1]
// Оба элемента — в одной кеш-линии (64 байта)
Когда flags[1] меняется, процессор инвалидирует кеш-линию на ядре Воркера 1. Тому приходится перечитывать данные из RAM. Это false sharing — задержка сотни циклов вместо единиц. Event loop ждет завершения воркеров, которые тратят 30% времени на перезагрузку кеш-линий, что блокирует цикл дольше. CPU загружен, а полезная работа не растет.
Методы предотвращения: padding и атомарность
Разделите данные padding, чтобы критические переменные попали в разные кеш-линии:
const CACHE_LINE_SIZE = 64; // байт
const PADDING = CACHE_LINE_SIZE / 4; // для Int32 (4 байта)
const flags = new Int32Array(1024 * 1024 * PADDING);
const idx = (i) => i * PADDING;
// Доступ: flags[idx(0)], flags[idx(1)]
Используйте Atomics с SharedArrayBuffer правильно — размещайте горячие переменные с отступами. Минимизируйте запись: вместо флагов применяйте очереди с атомарным каунтером для чтения.
Типичная ошибка и профилирование
Ошибка: надеяться, что Node.js сам управляет памятью потоков. На практике false sharing встречается в высоконагруженных воркерах. Профилируйте cache misses на Linux: perf stat -e cache-misses node app.js. Если miss ratio >5% — проблема в кеш-иерархии.
Вывод:
Понимание кеш-иерархии CPU и false sharing — must для сеньоров, работающих с high-load на Node.js, чтобы не допускать пробуксовки event loop из-за физики процессора. | 312 |
| 13 | Приоритезация микротасок в Node.js: когда event loop работает против тебя
Стандартный event loop обрабатывает очередь микротасок в порядке FIFO, что может стать проблемой при жестких SLA. Когда критичная операция — логирование ошибки, метрика или подтверждение платежа — стоит в очереди после десятка Promise.resolve() из менее важных цепочек, задержка становится неконтролируемой. Разработчики часто полагаются на process.nextTick или наивные очереди, не учитывая оверхед и риски блокировки цикла.
Async hooks с пользовательскими очередями
Через AsyncLocalStorage можно отслеживать контекст исполнения и разделить микротаски по приоритетам. Идея в создании "тихих" зон, где высокоприоритетные задачи обрабатываются вне основного потока.
const { AsyncLocalStorage } = require('async_hooks');
const als = new AsyncLocalStorage();
function createPriorityQueue() {
const queues = { high: [], low: [] };
return {
enqueue(fn, priority = 'low') {
queues[priority].push(fn);
if (queues[priority].length === 1)
queueMicrotask(() => drainQueue(priority));
}
};
}
Warning: async_hooks в production — не бесплатно. Каждый async-контекст добавляет накладные расходы на производительность. Используй точечно, например, только для критических путей.
Ручное управление очередями через queueMicrotask
Без оверхеда async_hooks можно реализовать собственную логику с батчингом:
let criticalQueue = [];
let normalQueue = [];
function processQueues() {
while (criticalQueue.length) criticalQueue.shift()();
if (normalQueue.length) {
const batch = normalQueue.splice(0, 10);
batch.forEach(fn => fn());
if (normalQueue.length) queueMicrotask(processQueues);
}
}
function schedule(fn, critical = false) {
if (critical) criticalQueue.push(fn);
else normalQueue.push(fn);
queueMicrotask(processQueues);
}
Когда это нужно в проде?
- Системы реального времени: WebSocket-сервера, game backends, где каждый миллисекунд важен.
- API с жесткими SLA: платежные шлюзы, трейдинговые платформы.
- Мониторинг: метрики не должны теряться из-за очереди логов.
Типичная ошибка: злоупотребление process.nextTick для приоритезации. Он выполняется перед всеми микротасками, но легко блокирует event loop при рекурсии.
Trade-off: ручное управление очередями — велосипед. Для production рассмотри библиотеки вроде denque для быстрых очередей без аллокаций. Worker Threads дают параллельность, но не приоритезацию в одном треде.
Вывод: Приоритезация микротасок через кастомные очереди — нишевый инструмент, который стоит внедрять только когда стандартный event loop не укладывается в метрики времени ответа, но требует тщательного контроля над оверхедом и возможным голоданием низкоприоритетных задач. | 265 |
| 14 | AbortController — больше чем отмена fetch. Асинхронные пайплайны без грязных утечек
В production — от SPA до Node.js сервисов — асинхронные пайплайны множатся, но отмену часто сводят к единичному сигналу для HTTP. Ошибка: считают, что AbortController решает всё автоматом, забывая про ручную очистку таймеров, стримов и сокетов. Ресурсы текут тихо, пока не наступит продакшн-баг с утечкой памяти.
Проверяй aborted после каждого await
Если пайплайн из нескольких шагов — не рассчитывай, что один сигнал магически остановит все. После каждого await проверяй signal.aborted. Иначе async-функции продолжат выполняться, пока не дойдут до точки, где AbortError всё же возникнет, но уже поздно.
Связывай сигналы для retry и композиций
При повторных попытках не плоди новые контроллеры в отрыве от внешнего. Привяжись через addEventListener с флагом once — так отмена пробрасывается корректно, а cleanup в catch предотвращает мёртвые таймеры:
async function retryWithAbort(url, signal) {
const inner = new AbortController();
signal.addEventListener('abort', () => inner.abort(), { once: true });
try {
return await fetch(url, { signal: inner.signal });
} catch (err) {
if (err.name === 'AbortError') cleanup();
throw err;
}
}
AbortSignal.any() — когда отмена приходит с разных сторон
Тайм-аут, действие пользователя, сигнал из микрофронтенда — объедини сигналы через AbortSignal.any(). Это снижает дублирование логики и упрощает очистку: один слушатель, один контроллер.
Вывод: Без явной проверки signal.aborted и явного cleanup в каждом звене асинхронный пайплайн гарантированно утекает, а отладка таких багов требует часов, а не минут. | 324 |
| 15 | Глубокое клонирование с сохранением типов: structuredClone vs. JSON vs. кастомная рекурсия для Map, Set, WeakMap и циклических ссылок
Глубокое клонирование — один из тех сценариев, где стандартные решения либо теряют типы, либо падают с ошибками. В production это критично: при клонировании состояния в стейт-менеджменте, кэшировании данных из API или передаче объектов между воркерами. Частая ошибка — полагаться на JSON.parse(JSON.stringify(obj)) во всех случаях, не учитывая, что он не поддерживает Map, Set, Date, BigInt и циклические ссылки.
✅ structuredClone — стандарт для большинства сценариев
Современный API, доступный в браузерах и Node.js 17+. Поддерживает Map, Set, ArrayBuffer, Blob, Date, RegExp, Error и циклические ссылки. Работает быстро, но не клонирует функции, WeakMap и WeakSet, а также теряет прототипы с геттерами и сеттерами. В production — базовый выбор.
const obj = {
a: 1,
map: new Map([['key', 'val']]),
date: new Date()
};
obj.self = obj;
const clone = structuredClone(obj);
// clone.map.get('key') -> 'val'
// clone.self === clone -> true
⚠️ JSON.parse/JSON.stringify — опасный компромисс
Подходит только для плоских объектов с примитивами. Потеря undefined, функций, BigInt, всех типов коллекций и падение на циклических ссылках делает его ненадёжным даже для простых API-ответов. Используйте только для чистых JSON-данных, где типобезопасность не требуется.
🔧 Кастомная рекурсия для полного контроля
Когда нужно сохранить прототипы, геттеры или работать с WeakMap/WeakSet, пишите рекурсию с WeakMap для защиты от циклов. Пример кукбука для экзотических типов:
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj);
let clone;
if (obj instanceof WeakMap) {
clone = new WeakMap();
hash.set(obj, clone);
obj.forEach((val, key) => clone.set(key, deepClone(val, hash)));
return clone;
}
if (obj instanceof Map) {
clone = new Map();
hash.set(obj, clone);
obj.forEach((val, key) => clone.set(key, deepClone(val, hash)));
return clone;
}
// Аналогично для Set, WeakSet
clone = Object.create(Object.getPrototypeOf(obj));
hash.set(obj, clone);
const descriptors = Object.getOwnPropertyDescriptors(obj);
for (const key of Object.keys(descriptors)) {
const desc = descriptors[key];
if (desc.get || desc.set) {
Object.defineProperty(clone, key, desc);
} else {
clone[key] = deepClone(desc.value, hash);
}
}
return clone;
}
Практический совет: для WeakMap аккуратно обрабатывайте ключи — они объекты, и их тоже нужно клонировать, иначе уникальность ссылок потеряется. Типичная ошибка — забыть обработать WeakMap в решении, что приводит к утечкам при кэшировании.
Вывод: structuredClone покрывает 90% сценариев, а кастомную рекурсию оставляйте для случаев, где важны прототипы, функции и WeakMap/WeakSet — тогда баланс между производительностью и типобезопасностью оправдан. | 254 |
| 16 | AsyncContext в Node.js: AsyncLocalStorage и автоматический trace-контекст без проброса аргументов
Как передать requestId через цепочку асинхронных вызовов, не прокидывая его в каждый аргумент? Классическая проблема в Node.js сервисах: middleware логирует requestId, сервисный слой его теряет. Прокидывать параметр через 5 уровней вложенности больно и ломает сигнатуры API. Многие тянут контекст через глобальные переменные или мутят DI-контейнеры, хотя есть встроенное решение.
Как это работает
AsyncLocalStorage из node:async_hooks создаёт изолированное хранилище, автоматически привязанное к текущему асинхронному контексту (цепочке Promise, таймеров, потоков). В production это позволяет убрать boilerplate:
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
const storage = new AsyncLocalStorage<{ requestId: string }>();
app.use((req, res, next) => {
storage.run(
{ requestId: req.headers['x-request-id'] || randomUUID() },
() => next()
);
});
function getRequestId(): string {
const store = storage.getStore();
if (!store) throw new Error('No async context');
return store.requestId;
}
async function processPayment(orderId: string) {
logger.info(Processing order ${orderId}, { requestId: getRequestId() });
}
Автоматический trace-контекст для логов
Для Pino или Winston можно подключить mixin — каждое сообщение лога автоматически получит requestId без ручного проброса:
const logger = pino({
mixin() {
const store = storage.getStore();
return store ? { requestId: store.requestId } : {};
}
});
Подводные камни
У решения есть границы:
- Не работает с коллбэками вне цепочки событий. Если Promise резолвится после завершения контекста — хранилище потеряно.
- Микротаски и nextTick работают корректно, а Worker Threads — нет. Контекст не наследуется, нужно пробрасывать вручную.
- Типичная ошибка: забывают обернуть асинхронный обработчик в storage.run(), и getStore возвращает undefined в runtime.
Вывод: AsyncLocalStorage — элегантная альтернатива DI-контейнерам для сквозного контекста в Node.js, которая делает код чище, тесты проще и интегрируется с OpenTelemetry для production-grade distributed tracing. | 288 |
| 17 | Регулярки в JS: когда pattern matching сжигает CPU
Регулярные выражения мощны, но неаккуратное использование в production — будь то валидация форм в SPA, парсинг данных на Node.js бэкенде или обработка логов — может привести к зависанию сервера. Самая частая ошибка — catastrophic backtracking, когда движок перебирает экспоненциальное количество комбинаций из-за вложенных квантификаторов.
Как возникает катастрофический возврат
Классический пример — валидация email:
/^([a-z]+)+@domain\.com$/
Пускаем строку aaaaaaaaaaaaaaaaaaaa!@domain.com. Движок не находит совпадение после ! и начинает перебирать варианты группировки внутри (a+)+: 20 символов, потом 19+1, 18+2... Получается ~2^20 путей. На 30 символах время выполнения растёт с миллисекунд до минут.
Необходимы три условия:
* Вложенные квантификаторы — + внутри + или * внутри *.
* Отсутствие фиксированного символа, прерывающего перебор.
* Неудачное совпадение в конце — движок возвращается к предыдущим вариантам.
Типичные паттерны-ловушки
Часто встречаются в коде парсинга HTML или валидации чисел:
/<(.+)>.+<\/\1>/ — разбор тегов
/^(-?\d+(\.\d+)?)+$/ — валидация чисел
Оба дают экспоненту на длинных строках с ошибкой. В боевом API-клиенте или SDK такое приведёт к зависанию при обработке вредоносного ввода.
Как защититься
Первый способ — эмулировать atomic groups через lookahead:
/(?=(a+))\1/ — захват без возврата
Второй — конкретизировать границы. Вместо .* используйте точные классы. Для email:
/^([a-z\d._%+-]+@[a-z\d.-]+\.[a-z]{2,})$/i
Третий — timeout. На Node.js используйте библиотеку re2 — она гарантирует O(n) и не залипает. Или оборачивайте регулярку в Web Worker с таймером.
Цифры и инструменты
На строке из 30 символов простая регулярка отрабатывает ~0.01ms, катастрофическая — >5000ms, рост экспоненциальный. Для дебага используйте regex101.com — там видно количество шагов. В Node.js можно увеличить --stack-size, но проще переписать паттерн.
Вывод:
Избегайте вложенных квантификаторов на одинаковых группах символов — это предотвратит catastrophic backtracking и спасёт CPU вашего production. | 271 |
| 18 | Gzip и Brotli — зло для стриминга. Почему продвинутые переходят на Zstd
Если ты используешь compression() из Express для всех API, ты ломаешь стриминг. Gzip и Brotli ждут полный буфер — res.write() по чанкам не сработает, сжатие начнётся только после res.end(). Это убивает SSE, пагинацию с курсорами и загрузку с прогрессом в production. Частая ошибка: разработчики ставят middleware сжатия глобально, не осознавая, что TTFB растет до момента полной отдачи ответа.
Компрессия против буферизации
app.use(compression()); // ждёт весь ответ
app.get('/stream', (req, res) => {
for (let i = 0; i < 1000; i++) res.write(chunk ${i}\n); // не сжимается
res.end(); // только тут сжатие
});
Gzip и Brotli требуют всего ответа для построения словаря. Для потоков данных — например, курсорной пагинации — это неприемлемо. Клиент не получит ни единого байта, пока сервер не завершит поток.
Zstd как решение для стриминга
Zstandard (RFC 8478) спроектирован для потокового сжатия: не накапливает буфер, сжимает на лету. Уровень 1 даёт ~10-20 ГБ/с, что достаточно для большинства серверов. Пример настройки через node-zstd:
import { Transform } from 'stream';
import { compress } from 'node-zstd';
const zstdStream = new Transform({
transform(chunk, _, callback) {
compress(chunk, 3)
.then(c => callback(null, c))
.catch(callback);
}
});
app.get('/api/items', (req, res) => {
res.setHeader('Content-Encoding', 'zstd');
getChunkedDataCursor().pipe(zstdStream).pipe(res);
});
Ключевой trade-off: не ставь уровень 22 — он жрёт память как Gzip -9. Для пагинации достаточно level 1-3 с window size 15 (32KB). Для статики можно 22 (4MB), но смотри по RPS.
Бенчмарки и практические советы
* Предкомпилированные словари для повторяющихся структур (DTO) дают до 40% ускорение на бенчмарках.
* На production:
| Алгоритм | TTFB (мс) | Throughput (MB/s) |
|----------|-----------|-------------------|
| Gzip -9 | 450 | 120 |
| Brotli -11| 890 | 85 |
| Zstd -3 | 78 | 980 |
Zstd даёт почти размер Gzip, но в 10 раз быстрее. Без блокировок на whole response — идеально для стриминга и пагинации.
Ошибка: не все браузеры поддерживают Zstd. Всегда проверяй Accept-Encoding и делай fallback на Brotli для клиентов. Для backend-to-backend коммуникации Zstd однозначно, как делает Netflix.
Вывод:
Gzip и Brotli оставь для статики с малым числом запросов; для стриминга, пагинации и SSE используй Zstd с Transform Stream — это даёт на порядок лучший TTFB и пропускную способность без компромиссов по сжатию. | 293 |
| 19 | Слабые ссылки и финализаторы: когда Rust не нужен, но GC бессилен
Управление памятью в JS — это иллюзия, пока не начнутся падения RSS в production. WeakRef и FinalizationRegistry — не инструменты для ежедневного кода, а спасение в сценариях, где классические паттерны утечек уже вылечены, а память продолжает расти. В production это актуально для long-running SPA, тяжёлых кэшей на Node.js и интеграций с нативными ресурсами вроде Canvas.
WeakRef: кэш, который убивает сам себя
Когда Map с картинками или JSON-ответами держит сильные ссылки — это утечка, если объект больше не нужен. WeakRef позволяет хранить ссылку, которую GC может забрать при нехватке памяти. Используйте для кэширования тяжёлых данных или слабых подписок в Observer-паттерне.
class SmartCache {
private data = new Map<string, WeakRef<object>>();
set(key: string, value: object) {
this.data.set(key, new WeakRef(value));
}
get(key: string): object | undefined {
return this.data.get(key)?.deref();
}
}
* Минус: .deref() может вернуть undefined в любой момент — обрабатывайте это явно.
FinalizationRegistry: cleanup для внешних ресурсов
Колбэк вызывается после сборки объекта. Нужен для освобождения WebSocket-соединений, файловых дескрипторов или памяти GPU. Не используйте для закрытия транзакций — колбэк может прийти с задержкой или не прийти до завершения процесса.
const registry = new FinalizationRegistry((id: string) => {
console.log(Cleanup: ${id});
});
class Heavy {
constructor(public id: string) {
registry.register(this, id);
}
}
* Типичная ошибка: делать в колбэке тяжёлую работу — V8 может зависнуть event loop.
Где без них реально не обойтись
* Long-running SPA с вкладками, которые не пересоздаются.
* Canvas/WebGL: ручное управление памятью — часть архитектуры.
* Node.js: кэши изображений или HLS-фрагментов в высоконагруженных сервисах.
Что ломается в production
* V8 реализует WeakRef с задержками — мгновенной очистки не ждите.
* FinalizationRegistry может вызвать колбэк не в том порядке.
* Это не замена грамотному управлению ссылками, а дополнительный слой для узких мест.
Как избежать боли
Напишите модуль-обёртку: внутри WeakRef и Registry, снаружи чистый API с fallback на пересоздание или ошибку. Разработчику не нужно знать о слабых ссылках. Тестируйте с помощью --expose-gc и следите за кучей в проде.
Вывод:
WeakRef — для кэшей и подписок, FinalizationRegistry — для внешних ресурсов, но оба требуют явной обработки исчезновения объекта и не отменяют необходимость профилирования памяти. | 265 |
| 20 | Утечки памяти в production: как отлавливать то, что не видно в логах
Каждый раз, когда слышу "у нас утечка памяти", первая мысль - кто-то забыл удалить слушатель событий или не очистил setInterval. В production все сложнее: утечки копятся неделями, проявляются при определенной нагрузке, и локально их не воспроизвести.
Вот три рабочих инструмента.
Heap snapshots: как читать между строк
Снимаешь снапшот в Chrome DevTools (Memory -> Take heap snapshot). Сравниваешь два: до и после выполнения подозрительного сценария.
Ищешь объекты, которые:
- Не должны быть в памяти, но есть (старые DOM-ноды с замыканиями)
- Имеют неожиданно много экземпляров (10 000 объектов класса Widget)
Если видишь Closure с огромным retained size - это замыкание, которое держит ссылку на внешние данные. В production такая утечка часто возникает в SSR, когда обработчик запроса захватывает ссылку на глобальный кэш.
Memory tab: интерактивный детектив
Переходишь на вкладку Memory, выбираешь "Allocation instrumentation on timeline". Записываешь профиль при активной работе пользователя.
Красные пики в столбчатой диаграмме - места, где память выделяется, но не освобождается. Клик по пику покажет стек вызовов в момент аллокации.
Паттерн, который ловил так: React-компонент, при каждом ререндере создавал новый объект-конфигурацию и передавал в children - GC не успевал чистить, через час работы страница весила 200+ МБ.
Автоматизированные паттерны детекции
Ручной анализ хорошо, но в production нужно автоматическое обнаружение. Два подхода.
Performance Observer с проверкой usedJSHeapSize:
new PerformanceObserver((list) => {
const entries = list.getEntries();
if (performance.memory?.usedJSHeapSize > 200_000_000) {
console.warn('Memory warning', performance.memory);
}
}).observe({ type: 'resource', buffered: true });
Только в Chrome с флагом enable-experimental-web-platform-features. Типичная ошибка - использовать это без fallback в Safari или Firefox.
Кастомный мониторинг:
setInterval(() => {
const { usedJSHeapSize, totalJSHeapSize } = performance.memory;
const usagePercent = (usedJSHeapSize / totalJSHeapSize) * 100;
if (usagePercent > 80) {
sendMetric('memory_leak_risk', usedJSHeapSize);
}
}, 60000);
Не используй для продакшена бездумно - performance.memory есть не везде. Оборачивай в try/catch и делай fallback на navigator.userAgent.
Утечки в production - это не баги, а архитектурные проблемы. Если после каждого перехода по роуту память растет на 5-10% и не падает - ищи подписки в эффектах, которые не отписываются при размонтировании. Или глобальные кэши, которые никогда не чистятся.
Вывод: Надежная детекция утечек требует сочетания автоматических инструментов и ручного анализа heap snapshots, а не надежды на GC или банальные логи. | 301 |
Уже доступно! Исследование Telegram 2025 — ключевые инсайты года 
