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

Настоящий JavaScript

Открыть в Telegram

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

Больше
6 208
Подписчики
-224 часа
-357 дней
-4830 день
Привлечение подписчиков
июнь '26
июнь '26
+110
в 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 каналах
Дата
Привлечение подписчиков
Упоминания
Каналы
30 июня+1
29 июня+3
28 июня+1
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
Посты канала
Как я собираю мини‑аналитику по рынку профессий Давно работая с HR‑аналитикой, стало интересно не просто смотреть на рынок, н
Как я собираю мини‑аналитику по рынку профессий Давно работая с HR‑аналитикой, стало интересно не просто смотреть на рынок, но и самому выделить что‑то основное, что можно собрать и представить: зарплатная аналитика, аналитика подбора персонала и тому подобное. Частные случаи отсутствия роста оплаты труда могут восприниматься людьми так, будто такое везде, но это может быть ошибкой. Год назад была достаточно сильная гонка зарплат, которая сейчас привела к акценту на производительности труда в стране. Многие ее не заметили. Безработица низкая, что означает дефицит кадров. Но сейчас не дефицит кадров вообще, а дефицит квалифицированных кадров и дефицит рабочих. Без данных такие фразы превращаются в ощущения, а ощущения — плохая основа для выводов. Поэтому начат сбор небольшого аналитического проекта по рынку профессий. Идея простая: брать открытые данные, приводить их в порядок и собирать короткие профили по отдельным профессиям. Читать далее

2
⁣Ambient type augmentation в declare module при динамической загрузке: скрытые конфликты с фактической типизацией и стратегии разрешения В монорепозиториях declare module в .d.ts файлах часто считают удобным способом типизировать динамически загружаемые модули. Однако на production это приводит к трудноуловимым багам, когда глобальные декларации из разных пакетов склеиваются, а не заменяются, создавая пересечения типов, которые не совпадают с реальным runtime-поведением. Проблема склейки в пересечение Когда два пакета в монорепозитории объявляют declare module 'lib' с разными версиями опций, TypeScript не выбирает одну — он объединяет их в пересечение. Результат: { mode: never }, если типы конфликтуют. Локально тесты проходят, а в production код падает из-за несовместимости. Типичная ошибка — считать, что declare module изолирован. Динамический импорт и runtime-разрыв При динамической загрузке через import('lib').then(...) TypeScript использует глобальные ambient-типы, а не фактическую сигнатуру модуля в рантайме. Если модуль загружается по условию (например, A/B-тест или фича-флаг), статически верный тип может не совпадать с реально загруженной версией. Результат — ошибки в рантайме, которые не ловятся статическим анализатором. Стратегии для монорепозиториев * Изоляция через tsconfig paths — явно указывай пути к реальным файлам модуля и исключай лишние .d.ts. Минус: накладные расходы на настройку каждого пакета. * Wrapper-пакет с type guard — создай прослойку, которая делает динамический импорт и экспортирует явный тип. Контролируешь, что отдаёшь наружу, но добавляешь лишний слой. * Маркер версии в declare module — добавь export const __version: '2.0.0' в декларацию. В коде видно, какая версия предполагалась, и можно проверить совпадение. Практический совет Периодически прогоняй tsc --noEmit --traceResolution в каждом пакете отдельно. Это покажет, откуда TypeScript берёт типы для declare module, и выявит случайные пересечения из node_modules или соседних пакетов. В монорепозиториях это обязательно, иначе баги с типами уйдут на production. Вывод: Ambient type augmentation в declare module — опасный компромисс между удобством и надёжностью типов в монорепозиториях, требующий явной изоляции через tsconfig paths или wrapper-пакеты для предотвращения runtime-ошибок.
187
3
⁣React Server Components в Next.js App Router: грабли, о которые споткнулись многие Даже на middle+ проектах RSC в Next.js подкидывают сюрпризы, которые ломают гидрацию, порождают race conditions в параллельных роутах и сбивают с толку типизацию Server Actions. Разберём три production-кейса, где интуиция подводит. Гидрация RSC: клиент оборачивает сервер Когда клиентский компонент оборачивает серверный, Next.js сначала рендерит всё на сервере, затем гидратирует на клиенте. Ошибка: если серверный компонент использует cookies() или headers(), а клиентский в это же время делает revalidation, данные расходятся — сервер видит один стейт, клиент другой. Фикс: добавь Suspense на границе между клиентским и серверным кодом или вынеси состояние в отдельный провайдер. Это не универсально, но покрывает 90% кейсов. Race conditions в параллельных роутах Parallel Routes выглядят мощно, но на практике легко получить состояние гонки. Когда два слота — children и modal — одновременно делают fetch() с ревалидацией, один может тихо перезаписать кэш другого. Особенно больно, если у них loading.tsx с разным временем загрузки. Решение: используй generateMetadata для детерминированной загрузки и вешай ключи вроде key={searchParams.tab} на слоты, чтобы принудительно перемаунтить компонент. Костыль, но работает. Типизация Server Actions Server Actions не наследуют типы при передаче в клиентские компоненты — TypeScript думает, что всё ок, а на деле тип съезжает, потенциально вызывая runtime-ошибку. Решение: вручную объявляй дженерик-тип: type ActionState = { success: boolean; error?: string }; type ServerAction = (state: ActionState, formData: FormData) => Promise<ActionState>; export const updateUser: ServerAction = async (state, formData) => { 'use server'; return { success: true }; }; Типобезопасно, но требует явной аннотации — не доверяй автоматическому выводу. Вывод: RSC в Next.js — мощный инструмент, но границы между клиентом и сервером, синхронизация кэша в параллельных роутах и явная типизация Server Actions требуют строгого инженерного контроля, иначе production ловит рассогласование данных.
194
4
⁣Как объявить Proxy типобезопасным: рефлексивные типы-обёртки для API, логирования и моков без потери IntelliSense Взяли Proxy, обернули API-клиент для логирования запросов. Вроде всё ок, пока не попытались вызвать метод с аргументами. IntelliSense молчит, типы потерялись, а в рантайме — undefined. Знакомо? Проблема в том, что new Proxy(target, handler) возвращает тип target как есть, но handler.get возвращает any, потому что тип prop — string | symbol. Компилятор не знает, что именно ты достаёшь, и подсказки пропадают. Решение: пересоздание типов через mapped types Не просто T, а структура, где каждый метод сохраняет свою сигнатуру: type TypedProxy<T> = { [K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => R : T[K]; }; Теперь createProxy возвращает объект, где IntelliSense видит те же аргументы и возврат, что у оригинала. В handler.get мы сами вызываем obj[prop](...args) — да, тут any, но наружу типы уже прокинуты нормально. Где это реально выручает * Логирование вызовов API без изменения типов ответов. * Моки для тестов — подменяешь метод, а интерфейс остаётся. * Валидация на set с проверкой типов через conditional types. Ловушки и компромиссы Если нужен доступ к динамическим полям вроде proxy[userInput], mapped types не помогут — придётся добавить index signature с [key: string]: unknown. IntelliSense для такого не заработает, это компромисс. Ещё одна ловушка: T extends object пропустит примитив. Лучше уточнять через T extends Record<string, any>, иначе прокси упадёт на числах или строках. Вывод: Без пересоздания типов Proxy остаётся чёрным ящиком; с mapped types — нормальный инструмент без потери подсказок, но с осознанием границ динамических полей.
250
5
На Stepik запустили мощный курс по «Troubleshooting Docker и Kubernetes: поиск и устранение проблем» В программе только важны
На Stepik запустили мощный курс по «Troubleshooting Docker и Kubernetes: поиск и устранение проблем» В программе только важные аспекты: — troubleshooting Docker и образов — диагностика сетевых проблем — настройка readiness/liveness probes — отладка pod’ов, деплоев и ingress — анализ логов контейнеров и кластера — разбор ошибок CrashLoopBackOff, OOMKilled, ImagePullBackOff и других Собеседования на DevOps/SRE сейчас всё чаще строятся вокруг реальных инцидентов. Данный курс фокусируется именно на таких сценариях и помогает в подготовке к практическим вопросам 48 часов доступен со скидкой 25% ↗️ Пройти курс на Stepik
237
6
⁣Deep dirty structuredClone: лимиты, нюансы сериализации и хаки для клонирования неподдерживаемых типов в production сценариях В production часто нужно глубокое копирование объектов, особенно при работе с состоянием в SPA, API клиентами или кэшированием в Node.js сервисах. Многие разработчики полагаются на structuredClone как на серебряную пулю, но забывают о его строгих ограничениях, которые могут привести к потере данных или багам в runtime. Что structuredClone копирует, а что нет Он успешно обрабатывает объекты, массивы, Map, Set, Date, RegExp, Blob, File и ArrayBuffer, включая циклические ссылки. Но есть черный список: функции, классы с методами, Symbol (как ключи и значения), DOM-элементы, а также Error объекты, которые клонируются как простые объекты с полями message и stack, но без прототипа. В production это проблема, например, при сериализации конфигов с колбэками или при работе с кастомными классами в shared libraries. Хаки для продакшна Для функций нужна ручная обвязка:function cloneWithFunctions(obj: Record<string, unknown>) { const clone = structuredClone(obj); for (const key of Object.keys(obj)) { if (typeof obj[key] === 'function') { clone[key] = obj[key]; } } return clone; } Этот подход грубый: замыкания привязаны к оригинальному скоупу, а не к копии. Полная изоляция через new Function или eval опасна и подходит только для ограниченных сценариев (например, серверные конфиги с проверенными функциями). Для DOM-элементов используйте cloneNode(true). Для Error восстанавливайте прототип: function cloneError(err: Error): Error { return Object.assign(Object.create(Error.prototype), err, { message: err.message, name: err.name, stack: err.stack }); } Но это меняет поведение — оригинальный Error мог иметь кастомные свойства, которые не сериализуются. Типичная ошибка Нельзя использовать structuredClone для клонирования объектов с кастомными прототипами, например, экземпляров классов с методами, или для сохранения ссылочной идентичности. Все копии становятся независимыми плоскими объектами. Symbol-ключи полностью теряются. В бандлинге или дизайн-системах это ведет к неожиданным багам, когда копия не имеет ожидаемого поведения или ломает инварианты модуля. Вывод: structuredClone надежен только для изолированных данных без функций, Symbol и кастомных прототипов — для production-сценариев с артефактами runtime всегда делайте ручную обвязку и документируйте границы клонирования.
208
7
⁣Кеш для 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.
225
8
⁣Вложенные 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 при долгоживущих подписках.
191
9
⁣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.
209
10
⁣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 из системы типов в инструмент настоящей инкапсуляции, где приватность гарантируется на уровне исполнения, а не только компиляции.
208
11
🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы Внутри статьи она подробно расписывает этапы собес
🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы Внутри статьи она подробно расписывает этапы собеседований, лайфхаки и делится учебными ресурсами, которые ей помогли. Плюс девушка великодушно оставила ссылки на свой Notion с полезными заметками по математике и LLM. ✖️ xCode Journal
285
12
⁣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-надёжности.
250
13
⁣Строгая типизация 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.
256
14
😁 Пункта про стоимость и требуемые характеристики к железу не хватает ✖️ xCode Journal
😁 Пункта про стоимость и требуемые характеристики к железу не хватает ✖️ xCode Journal
308
15
⁣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-среде.
347
16
⁣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 — это убережет от невидимых дефектов в числовых данных.
316
17
День сурка frontend-разработчика Зарплата стоит, скучные задачи день за днем, календарь забит созвонами, которые не влияют во
День сурка frontend-разработчика Зарплата стоит, скучные задачи день за днем, календарь забит созвонами, которые не влияют вообще ни на что. Откликаешься на вакансии, а в ответ тишина либо какие-то мутные конторы. На собесах вместо нормальной оценки навыков цирк с алгоритмами на скорость, как будто ты на олимпиаде, а не работу ищешь. И самое неприятное, пока ты варишься в этом болоте, кто-то спокойно проходит собесы и уходит в Яндекс, VK или на хорошую Валютную удаленку без лишней драмы. Есть классные проекты и сильные команды, где разработчиков действительно ценят, дают расти, поддерживают развитие и платят достойно и ты можешь туда попасть! 👋 Меня зовут Тихон, привет! Я — действующий Frontend-разработчик и ментор. Я за руку довожу до оффера на хорошую позицию в Big Tech и сопровождаю на испытательном сроке. Также из учеников я собираю комьюнити, где уже более 220 frontend-разработчиков🫂 А в своем канале: 👉Объясняю, как проходить HR-фильтр и превращать отклики в реальные приглашения 👉Помогаю найти мотивацию, борюсь убеждениями, которые мешают развиваться 👉На примерах объясняю, как проходить собеседования, включая техничку 👉Разбираю резюме и делюсь лайфхаками, например как аккуратно “пинговать” рекрутеров А еще регулярно публикую полезные материалы: ▪️Задачи, на которых валяться кандидаты ▪️База по микрофронтам ▪️Подборка из 100+ каналов с вакансиями для разработчиков ▪️100 вопросов, которые точно помогут тебе на собеседовании ▪️Чек лист проверки своего резюме А еще у меня множество успешных кейсов и отзывов, найти их можно в канале. Реклама, erid: 2W5zFJixYDf ИП Галактионов Тихон Витальевич, ИНН 771618975809
265
18
⁣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 из-за физики процессора.
325
19
⁣Приоритезация микротасок в 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 не укладывается в метрики времени ответа, но требует тщательного контроля над оверхедом и возможным голоданием низкоприоритетных задач.
312
20
⁣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 в каждом звене асинхронный пайплайн гарантированно утекает, а отладка таких багов требует часов, а не минут.
349