ru
Feedback
Настоящий JavaScript

Настоящий 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 и поделилась своим опытом поиска работы Внутри статьи она подробно расписывает этапы собес
🤯 Девушка получила оффер в 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
😁 Пункта про стоимость и требуемые характеристики к железу не хватает ✖️ 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-разработчика Зарплата стоит, скучные задачи день за днем, календарь забит созвонами, которые не влияют во
День сурка 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 для всех
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 — это иллюзия, пока не начнутся пад
Слабые ссылки и финализаторы: когда 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: как отлавливать то, что не видно в логах Каждый раз, когда слышу "у нас утечка памяти", первая мы
Утечки памяти в 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