Frontender's notes [ru]

Frontender's notes [ru]

@frontendnoteschannel_ru

Ведущий канал о современном фронтенде: статьи, новости, практики, вайбкодинг и автоматизация фронта ИИ-агентами.Личный блог автора - @just_genychПо вопросам рекламы или разработки - @g_abashkin

32 381подписчиков
Несколько раз в неделюmixed

Похожие каналы

Все →

Последние посты

Frontender's notes [ru] — пост в ТГ канале

bfcache в SPA: как не потерять мгновенный back/forward UX и не проснуться со старым состояниемbfcache - не кэш данных, а заморозка всей страницы: DOM, JS heap, scroll, router state и React/Vue-приложение. В production он ускоряет возврат назад/вперед, но часто ломается из-за cleanup «на всякий случай» или восстанавливает устаревшую сессию.Диагностика восстановленияГлавный сигнал - pageshow. При возврате из bfcache load не сработает, поэтому bootstrap, завязанный только на DOMContentLoaded / load, пропустит важную логику.window.addEventListener('pageshow', e => { if (e.persisted) { revalidateCriticalQueries(); syncAuthState(); reconnectRealtime(); resumeTimers(); }});window.addEventListener('pagehide', e => { if (e.persisted) { pausePolling(); closeEphemeralConnections(); }});Что блокирует bfcacheЧастые причины:* unload handler;* лишний beforeunload;* WebSocket, WebRTC, BroadcastChannel, Web Locks;* незавершенные IndexedDB-транзакции;* активные fetch/stream-операции;* Cache-Control: no-store;* сторонние скрипты с unload.Типичная ошибка SPA - вешать unload для cleanup. Используйте pagehide, но не уничтожайте store и router state, если event.persisted === true.Как искать причиныВ Chrome DevTools откройте Application - Back/forward cache и запустите тест навигации. DevTools покажет blockers: active connection, unsupported feature, unload handler.Для логов используйте pageshow.persisted и pagehide.persisted. performance.getEntriesByType('navigation')[0].type === 'back_forward' полезен, но не гарантирует успешный restore из bfcache.Что восстанавливать после pageshowПроверяйте только внешнее состояние: auth/session, критичные query cache, realtime, polling, feature flags, time-based данные и синхронизацию вкладок.Вывод:bfcache - это продолжение жизни страницы, поэтому надежная SPA не перезапускается целиком, а точечно возобновляет ресурсы и пересинхронизирует данные.

17 июн. 2026 г.2 070В Telegram
Frontender's notes [ru] — пост в ТГ канале

Fetch Priority в production: как управлять LCP-ресурсами без starvation и регрессий загрузкиfetchpriority — это не «ускоритель сайта», а hint для браузера: какой ресурс стоит поднять или опустить в очереди загрузки. В production он полезен прежде всего для LCP-ресурсов: hero image, poster, иногда preload шрифта/критичного ассета.Главная ошибка — ставить fetchpriority="high" на всё важное. Если всё high, то high не значит ничего. Более того, можно получить starvation: hero начнёт конкурировать с preload’ами, скриптами, шрифтами, картинками карусели и ухудшит загрузку остального критичного пути.Практическое правило:1. Поднимать только реальный LCP-кандидат Один viewport, один главный ресурс.<img src="/hero.avif" srcset="/hero-768.avif 768w, /hero-1440.avif 1440w" sizes="100vw" width="1440" height="720" fetchpriority="high" decoding="async" alt="">2. Если LCP-ресурс поздно обнаруживается — используйте preload + fetchpriorityНапример, hero приходит из CSS background или рендерится поздно после JS. Лучше сделать ресурс discoverable раньше:<link rel="preload" as="image" href="/hero.avif" fetchpriority="high"/>Но не дублируйте бездумно: href, imagesrcset, imagesizes, type должны совпадать с реальным ресурсом, иначе можно скачать не то или скачать дважды.3. Не повышайте приоритет всем картинкам above the foldТипичный анти-паттерн:<img src="/slide-1.avif" fetchpriority="high"><img src="/slide-2.avif" fetchpriority="high"><img src="/slide-3.avif" fetchpriority="high">Если виден только первый слайд — high должен быть только у него. Остальные: loading="lazy" или обычный auto.4. Опускайте второстепенное, но аккуратноfetchpriority="low" может быть полезен для изображений, которые браузер обнаруживает рано, но которые не нужны для первого экрана: декоративные картинки, hidden-состояния, неактивные слайды.<img src="/promo-secondary.avif" fetchpriority="low" loading="lazy" alt="">Но не ставьте low на ресурсы, которые могут стать LCP на других брейкпоинтах. Иначе на mob

16 июн. 2026 г.2 000В Telegram

⁣⁣🔐 Координация вкладок через BroadcastChannel и Web Locks: refresh токенов без гонокКлассическая проблема SPA с несколькими вкладками:1. Access token истёк.2. Пользователь открывает 3 вкладки.3. Все вкладки одновременно получают 401.4. Все делают /auth/refresh.5. Если refresh token ротируется — первая вкладка обновила его, остальные уже пришли со старым refresh token.6. Итог: logout, invalid_grant, странные баги авторизации.Решение: сделать refresh single-flight на весь origin.Для этого удобно совместить два API:- Web Locks — гарантирует, что refresh выполняет только одна вкладка.- BroadcastChannel — быстро сообщает остальным вкладкам результат refresh.Идея простая:- каждая вкладка перед refresh пытается взять lock;- внутри lock обязательно повторно проверяет, не обновил ли токен кто-то другой;- после успешного refresh рассылает новый access token/метаданные в остальные вкладки;- остальные вкладки обновляют своё состояние и не делают лишний refresh.Пример:const channel = new BroadcastChannel('auth');let accessToken: string | null = null;let accessTokenExp = 0;const REFRESH_LOCK = 'auth-refresh-lock';const SKEW = 30_000;function isAccessTokenFresh() { return accessToken && accessTokenExp - Date.now() > SKEW;}function applySession(session: { accessToken: string; accessTokenExp: number;}) { accessToken = session.accessToken; accessTokenExp = session.accessTokenExp;}channel.onmessage = (event) => { const message = event.data; if (message.type === 'auth:refreshed') { applySession(message.session); } if (message.type === 'auth:refresh-failed') { accessToken = null; accessTokenExp = 0; }};async function refreshSession() { const response = await fetch('/auth/refresh', { method: 'POST', credentials: 'include', }); if (!response.ok) { throw new Error('Refresh failed'); } return response.json();}export async function getAccessToken() { if (isAccessTokenFresh()) { return accessToken; } return navigator.locks.request( REFRESH_LOCK,

15 июн. 2026 г.2 190В Telegram
Frontender's notes [ru] — пост в ТГ канале

CSS Anchor Positioning — это то, чего давно не хватало для тултипов, поповеров и меню: привязка одного элемента к другому на уровне CSS, без ручного расчёта координат в JS.Раньше типичный flow был такой:1. getBoundingClientRect()2. посчитать top / left3. записать inline-стили4. пересчитать при scroll / resize / zoom / изменении контента5. случайно получить layout thrashingОсобенно неприятно, когда таких тултипов десятки, они живут в порталах, попадают в top layer или должны флипаться при нехватке места.CSS Anchor Positioning решает именно эту задачу: элемент объявляется «якорем», а другой элемент позиционируется относительно него.Минимальный пример:<button class="help" popovertarget="tip"> ?</button><div id="tip" popover class="tooltip"> Подсказка без JS-позиционирования</div>.help { anchor-name: --help;}.tooltip { position: absolute; position-anchor: --help; inset-block-start: anchor(block-end); inset-inline-start: anchor(center); translate: -50% 8px; margin: 0; padding: 8px 12px; border-radius: 8px; background: #111; color: white;}Что здесь происходит:- .help становится anchor-элементом через anchor-name- .tooltip привязывается к нему через position-anchor- anchor(block-end) берёт нижнюю границу кнопки- anchor(center) берёт горизонтальный центр кнопки- translate добавляет смещение для красивого отступаJS при этом может заниматься только поведением: открыть / закрыть поповер, обработать action, синхронизировать состояние. Но не измерять DOM и не писать координаты.В этом и главная ценность: positioning переезжает туда, где ему место — в layout engine браузера.Почему это важно для производительности:- меньше getBoundingClientRect() в рантайме- меньше forced reflow- меньше resize / scroll listeners- меньше кастомной логики для пересчёта позиции- проще работать с Popover API и top layer- меньше зависимости от тяжёлых positioning-библиотек там, где нужна простая привязкаДля сложных кейсов вроде dropdown-меню, context menu или rich tooltip всё ещё могут пона

14 июн. 2026 г.2 470В Telegram

⁣AsyncLocalStorage в Node.js: request-scoped контекст для логов, трассировки и транзакций без prop drillingВ production это нужно в Node.js-сервисах, SSR, API integration и backend-for-frontend слоях, где requestId, traceId, tenant или транзакция должны проходить через async-код. Частая ошибка - протаскивать ctx через десятки методов или, наоборот, прятать в нём бизнес-состояние.Как это выглядитAsyncLocalStorage использует async_hooks и привязывает store к async execution flow: promise, timeout, I/O callback и большинству стандартных API Node.js.type Ctx = { requestId: string; traceId?: string; tx?: unknown };const als = new AsyncLocalStorage<Ctx>();app.use((req, _res, next) => { als.run({ requestId: req.headers['x-request-id']?.toString() ?? randomUUID(), traceId: req.headers.traceparent?.toString(), }, next);});const getCtx = () => als.getStore();function logInfo(msg: string, meta = {}) { const c = getCtx(); logger.info({ requestId: c?.requestId, traceId: c?.traceId, ...meta, }, msg);}Теперь сервисный код не знает про HTTP middleware и req, но логи автоматически получают correlation metadata.Транзакции без протаскивания txДля DB слоя можно делать withTransaction(), который запускает als.run({ ...ctx, tx }, fn), а getDbExecutor() возвращает ctx.tx ?? db. Trade-off хороший: меньше шума в API сервисов, но граница ответственности остаётся инфраструктурной, а не бизнесовой.Где границы* не заменяйте аргументы функции: бизнес-данные передавайте явно;* не кладите в store req, res, большие payload или ORM graph;* context не пересекает process boundary: worker threads, очереди, cron jobs и другие сервисы требуют явной передачи ids;* нестандартные callback-based библиотеки могут разорвать async chain.Практическое правило: используйте AsyncLocalStorage для логов, tracing, tenant/user metadata, аудита и текущей транзакции, а не для скрытого глобального состояния.Вывод:AsyncLocalStorage полезен, когда request-scoped инфраструктурный контекст улучшает observa

13 июн. 2026 г.2 690В Telegram
Frontender's notes [ru] — пост в ТГ канале

AbortSignal.any() и AbortSignal.timeout(): единая отмена fetch, таймеров и async-операций в productionPromise.race([fetch(), timeout]) часто маскирует проблему: ждать перестали, но работа могла не остановиться. В SPA, SSR, Node.js-сервисах, SDK и API clients это приводит к висящим запросам, таймерам и retry/backoff ниже по стеку.Собирайте отмену вокруг AbortSignalAbortSignal.timeout(ms) сам отменится по таймауту.AbortSignal.any([...signals]) отменится, когда отменится любой входной signal.Так в один контракт попадают:* caller отменил операцию* истек timeout* клиент закрыл соединение* сервис уходит в graceful shutdownconst shutdown = new AbortController();async function loadUser(id: string, opts: { signal?: AbortSignal } = {}) { const signal = AbortSignal.any([ AbortSignal.timeout(1500), shutdown.signal, ...(opts.signal ? [opts.signal] : []), ]); signal.throwIfAborted(); const res = await fetch(`https://api.example.com/users/${id}`, { signal }); await sleep(100, signal); // backoff тоже отменяем const data = await res.json(); signal.throwIfAborted(); return data;}Типичная ошибкаНе останавливайтесь на fetch. Один и тот же signal стоит передавать в retry, polling, очереди, sleep/timer helpers и свои async-функции. Иначе верхний слой "отменился", а нижний продолжает держать ресурсы.Практические нюансы* AbortSignal одноразовый: если aborted === true, нужен новый controller или timeout* AbortSignal.any() - это fan-in, а не fan-out: он не отменяет исходные контроллеры* смотрите на reason: timeout часто дает TimeoutError, abort - AbortError или ваш reason* при обертках над setTimeout, stream, listener или socket чистите ресурсы при abortВывод:Отмена должна быть частью контракта async-функции, а не локальным Promise.race на краю системы.

11 июн. 2026 г.3 370В Telegram
Frontender's notes [ru] — пост в ТГ канале

Страшная тайна российского айти✖️ xCode Journal

10 июн. 2026 г.3 780В Telegram

⁣Variadic tuple types — сложные сигнатуры без болиДо variadic tuple typesмногие сложные сигнатуры в TypeScriptвыглядели как наказание.Особенно:👉 curry 👉 compose 👉 middleware 👉 typed event emitter 👉 любые функции с «прокинь аргументы дальше» Приходилось писать overload на overloadи дублировать типы вручную.Как было раньшеОбычно появлялись:👉 overload на overload 👉 ручные tuple-типы 👉 тонны дублирования Типы быстро превращалисьв нечитаемую простыню.Что изменили variadic tuplesС их появлением стало намного прощеработать с остаточными аргументами на уровне типов.Например:type Fn<T extends unknown[]> = (...args: [...T]) => voidИли собирать сигнатуры:type Append<Args extends unknown[], Arg> = [...Args, Arg]Типы наконец научились нормально работатьс «переменным количеством аргументов».Почему это важноНа практике это одна из тех TS-фич,которые реально упростили жизнь библиотекам.Без variadic tuples:👉 Redux middleware typings 👉 router APIs 👉 compose/curry utilities были бы ещё страшнее.Где начинается тёмная магияПроблемы начинаются,когда variadic tuples комбинируют с:👉 infer 👉 recursive types 👉 conditional types Типовая система очень быстропревращается в тёмный лес.IDE начинает тормозить,ошибки становятся нечитаемыми,а compile time — расти.Главная мысльVariadic tuple types —это действительно мощная фича.Главное —вовремя остановитьсяи не превратить типы в отдельный язык программирования.

9 июн. 2026 г.3 590В Telegram
Frontender's notes [ru] — пост в ТГ канале

🤣 Не баг, а фича✖️ xCode Journal

3 июн. 2026 г.4 800В Telegram
Frontender's notes [ru] — пост в ТГ канале

Мой код после сотен правок и костылей

2 июн. 2026 г.5 470В Telegram

⁣ES2025: Импорт JSON-файлов как модулейВведениеС выходом ECMAScript 2025 (ES2025) разработчики получили возможность напрямую импортировать JSON-файлы как модули в JavaScript-коде. Это упрощает работу с конфигурационными данными и другими статическими ресурсами, представленными в формате JSON.Синтаксис импорта JSON-модулейДля импорта JSON-файла используется ключевое слово import с указанием атрибута with { type: 'json' }. Это гарантирует, что импортируемый файл будет обработан как JSON-модуль.Пример использованияРассмотрим пример импорта конфигурационного файла config.json и обращения к его свойствам в коде.import config from './config.json' with { type: 'json' };.log(config.apiUrl); // Выводит значение свойства apiUrl из config.jsonconsole.log(config.timeout); // Выводит значение свойства timeout из config.json❗️Добавление поддержки импорта JSON-файлов как модулей в ES2025 упрощает работу с данными в формате JSON, делая код более чистым и понятным.ИсточникиJSON Modules Can Now Be Imported in JavaScript in All Modern Browsers, CSS Modules to Follow. New Features in ES2025 – BooleanBuffer.

29 мая 2026 г.5 930В Telegram

⁣Recursive type limits — почему TS иногда «умирает»В TypeScript можно написать тип,который выглядит красиво,но заставляет компилятор страдать.Особенно когда начинаются рекурсивные типы.Например:👉 глубокий DeepPartial 👉 парсинг строк на уровне типов 👉 сложные conditional types 👉 infer внутри infer На маленьком примере всё работает.В реальном проекте IDE внезапно начинает думать по 5 секунд.Почему так происходитTypeScript не вычисляет типы «бесплатно».Каждый:👉 conditional type 👉 union 👉 recursive шаг нужно реально посчитать.А если тип разворачивается слишком глубоко,компилятор упирается в лимиты.Отсюда знакомое:Type instantiation is excessively deepand possibly infiniteИ это не всегда баг TypeScript.Часто это сигнал,что типовая модель стала слишком умной.Где обычно всё ломаетсяОсобенно опасны:👉 рекурсивные mapped types 👉 огромные union’ы 👉 type-level parser’ы 👉 deeply nested generics 👉 utility types поверх utility types Типы начинают взрываться комбинаторно.Что обычно помогает👉 не делать type-level акробатику без нужды 👉 ограничивать глубину рекурсии 👉 разбивать типы на более простые 👉 добавлять явные промежуточные типы 👉 не тащить сложные generic-типы в публичный API Почему это важноСложные типы бьют не только по компиляции.Они ухудшают:👉 autocomplete 👉 responsiveness IDE 👉 читаемость кода 👉 onboarding новых людей Иногда самый дорогой runtime —это compile time.Главная мысльХороший TypeScript —это не когда типы поражают воображение.Хороший TypeScript —это когда их можно понять через полгода,а IDE при этом не превращается в обогреватель.

28 мая 2026 г.4 420В Telegram

⁣Isolated declarations — ускорение больших monorepoВ TypeScript есть флаг isolatedDeclarations.Он нужен не для красоты типов,а для скорости.Проблема простая:в больших monorepo генерация .d.tsможет становиться узким местом.TypeScript часто должен анализировать соседние файлы,чтобы понять, какие декларации вывести.На маленьком проекте это почти незаметно.На большом — начинает болеть.Что делает isolatedDeclarationsisolatedDeclarations заставляет писать код так,чтобы декларации можно было генерироватьпо файлам независимо.Из-за этого TypeScript чаще требует явные типы.Было:export function getUser() { return { id: 1, name: 'Alex' }}Лучше так:type User = { id: number name: string}export function getUser(): User { return { id: 1, name: 'Alex' }}Меньше магии для компилятора —быстрее и предсказуемее сборка.Почему это важноКогда проект растёт:👉 TypeScript начинает сильнее зависеть от соседних файлов 👉 инкрементальная сборка замедляется 👉 генерация типов становится дорогой Изоляция помогает компилятору работать параллельно и проще.Где это особенно полезно👉 большие monorepo 👉 библиотеки 👉 project references 👉 параллельная сборка 👉 CI, где каждая минута стоит денег Главный trade-offТы немного платишь:👉 более явными типами 👉 меньшим type inference 👉 дополнительным boilerplate Но взамен получаешь:👉 более быстрые сборки 👉 стабильный compile pipeline 👉 меньше скрытой сложности Главная мысльЭто хороший пример взрослого engineering trade-off:чуть больше явности в кодеради скорости и предсказуемости системы.

27 мая 2026 г.3 530В Telegram
Frontender's notes [ru] — пост в ТГ канале

🤣 Мем отлично отражает настроения в сообществе прямо сейчас✖️ xCode Journal

25 мая 2026 г.3 930В Telegram

⁣starting-style — правильные enter animations без JSВ CSS появился @starting-style.Он решает старую проблему:анимацию появления элемента,который только что добавили в DOM.Раньше для этого обычно:👉 делали setTimeout 👉 дёргали классы через JS 👉 ловили reflow-хаки Теперь можно нативно:.modal { opacity: 1; transition: opacity .2s;}@starting-style { .modal { opacity: 0; }}Элемент появляется сразу с анимацией.Без двойного рендера и JS-костылей.Почему это важноРаньше браузер не успевал увидетьначальное состояние элемента.Из-за этого приходилось:👉 форсить reflow 👉 откладывать изменение класса 👉 городить лишнюю логику Теперь CSS сам понимает:👉 с какого состояния начинать transition Где особенно полезно👉 dialog 👉 popover 👉 conditional rendering 👉 enter animations в UI Почему это ощущается приятноАнимации становятся:👉 чище 👉 предсказуемее 👉 без лишнего JS CSS постепенно забирает себе всё,что раньше приходилось городить через JavaScript.

22 мая 2026 г.4 490В Telegram