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

Настоящий JavaScript

Открыть в Telegram

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

Больше
6 213
Подписчики
-824 часа
-317 дней
-3730 день
Привлечение подписчиков
июнь '26
июнь '26
+106
в 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 каналах
Дата
Привлечение подписчиков
Упоминания
Каналы
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
Посты канала
Как объявить 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 — нормальный инструмент без потери подсказок, но с осознанием границ динамических полей.

2
На Stepik запустили мощный курс по «Troubleshooting Docker и Kubernetes: поиск и устранение проблем» В программе только важны
На Stepik запустили мощный курс по «Troubleshooting Docker и Kubernetes: поиск и устранение проблем» В программе только важные аспекты: — troubleshooting Docker и образов — диагностика сетевых проблем — настройка readiness/liveness probes — отладка pod’ов, деплоев и ingress — анализ логов контейнеров и кластера — разбор ошибок CrashLoopBackOff, OOMKilled, ImagePullBackOff и других Собеседования на DevOps/SRE сейчас всё чаще строятся вокруг реальных инцидентов. Данный курс фокусируется именно на таких сценариях и помогает в подготовке к практическим вопросам 48 часов доступен со скидкой 25% ↗️ Пройти курс на Stepik
174
3
⁣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 всегда делайте ручную обвязку и документируйте границы клонирования.
162
4
⁣Кеш для 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.
205
5
⁣Вложенные 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 при долгоживущих подписках.
178
6
⁣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.
190
7
⁣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 из системы типов в инструмент настоящей инкапсуляции, где приватность гарантируется на уровне исполнения, а не только компиляции.
189
8
🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы Внутри статьи она подробно расписывает этапы собес
🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы Внутри статьи она подробно расписывает этапы собеседований, лайфхаки и делится учебными ресурсами, которые ей помогли. Плюс девушка великодушно оставила ссылки на свой Notion с полезными заметками по математике и LLM. ✖️ xCode Journal
265
9
⁣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-надёжности.
238
10
⁣Строгая типизация 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.
245
11
😁 Пункта про стоимость и требуемые характеристики к железу не хватает ✖️ xCode Journal
😁 Пункта про стоимость и требуемые характеристики к железу не хватает ✖️ xCode Journal
278
12
⁣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-среде.
308
13
⁣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 — это убережет от невидимых дефектов в числовых данных.
291
14
День сурка frontend-разработчика Зарплата стоит, скучные задачи день за днем, календарь забит созвонами, которые не влияют во
День сурка frontend-разработчика Зарплата стоит, скучные задачи день за днем, календарь забит созвонами, которые не влияют вообще ни на что. Откликаешься на вакансии, а в ответ тишина либо какие-то мутные конторы. На собесах вместо нормальной оценки навыков цирк с алгоритмами на скорость, как будто ты на олимпиаде, а не работу ищешь. И самое неприятное, пока ты варишься в этом болоте, кто-то спокойно проходит собесы и уходит в Яндекс, VK или на хорошую Валютную удаленку без лишней драмы. Есть классные проекты и сильные команды, где разработчиков действительно ценят, дают расти, поддерживают развитие и платят достойно и ты можешь туда попасть! 👋 Меня зовут Тихон, привет! Я — действующий Frontend-разработчик и ментор. Я за руку довожу до оффера на хорошую позицию в Big Tech и сопровождаю на испытательном сроке. Также из учеников я собираю комьюнити, где уже более 220 frontend-разработчиков🫂 А в своем канале: 👉Объясняю, как проходить HR-фильтр и превращать отклики в реальные приглашения 👉Помогаю найти мотивацию, борюсь убеждениями, которые мешают развиваться 👉На примерах объясняю, как проходить собеседования, включая техничку 👉Разбираю резюме и делюсь лайфхаками, например как аккуратно “пинговать” рекрутеров А еще регулярно публикую полезные материалы: ▪️Задачи, на которых валяться кандидаты ▪️База по микрофронтам ▪️Подборка из 100+ каналов с вакансиями для разработчиков ▪️100 вопросов, которые точно помогут тебе на собеседовании ▪️Чек лист проверки своего резюме А еще у меня множество успешных кейсов и отзывов, найти их можно в канале. Реклама, erid: 2W5zFJixYDf ИП Галактионов Тихон Витальевич, ИНН 771618975809
265
15
⁣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 из-за физики процессора.
321
16
⁣Приоритезация микротасок в 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 не укладывается в метрики времени ответа, но требует тщательного контроля над оверхедом и возможным голоданием низкоприоритетных задач.
297
17
⁣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 в каждом звене асинхронный пайплайн гарантированно утекает, а отладка таких багов требует часов, а не минут.
340
18
⁣Глубокое клонирование с сохранением типов: 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 — тогда баланс между производительностью и типобезопасностью оправдан.
285
19
⁣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.
295
20
⁣Регулярки в 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.
280