ch
Feedback
Frontender's notes [ru]

Frontender's notes [ru]

前往频道在 Telegram

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

显示更多

📈 Telegram 频道 Frontender's notes [ru] 的分析概览

频道 Frontender's notes [ru] (@frontendnoteschannel_ru) 俄语 语言赛道中的 是活跃参与者。目前社区聚集了 32 263 名订阅者,在 技术与应用 类别中位列第 4 210,并在 俄罗斯 地区排名第 20 093

📊 受众指标与增长动态

невідомо 创建以来,项目保持高速增长,吸引了 32 263 名订阅者。

根据 01 七月, 2026 的最新数据,频道保持稳定运转。过去 30 天订阅人数变化为 -337,过去 24 小时变化为 -13,整体触达仍然可观。

  • 认证状态: 未认证
  • 互动率 (ER): 平均受众互动率为 7.48%。内容发布后 24 小时内通常能获得 4.62% 的反应,占订阅者总量。
  • 帖子覆盖: 每篇帖子平均可获得 2 414 次浏览,首日通常累积 1 490 次浏览。
  • 互动与反馈: 受众积极参与,单帖平均反应数为 12
  • 主题关注点: 内容集中在 браузер, api, css, интерфейс, загрузка 等核心主题上。

📝 描述与内容策略

作者将该频道定位为表达主观观点的平台:
Ведущий канал о современном фронтенде: статьи, новости, практики, вайбкодинг и автоматизация фронта ИИ-агентами. Личный блог автора - @just_genych По вопросам рекламы или разработки - @g_abashkin

凭借高频更新(最新数据采集于 02 七月, 2026),频道始终保持新鲜度与高覆盖。分析显示受众积极互动,使其成为 技术与应用 类别中的关键影响点。

32 263
订阅者
-1324 小时
-877
-33730
吸引订阅者
七月 '26
七月 '260
在0个频道中
六月 '26
+111
在0个频道中
Get PRO
五月 '26
+239
在22个频道中
Get PRO
四月 '26
+300
在0个频道中
Get PRO
三月 '26
+136
在1个频道中
Get PRO
二月 '26
+156
在1个频道中
Get PRO
一月 '26
+250
在60个频道中
Get PRO
十二月 '25
+319
在28个频道中
Get PRO
十一月 '25
+379
在69个频道中
Get PRO
十月 '25
+210
在32个频道中
Get PRO
九月 '25
+265
在90个频道中
Get PRO
八月 '25
+111
在6个频道中
Get PRO
七月 '25
+223
在2个频道中
Get PRO
六月 '25
+247
在9个频道中
Get PRO
五月 '25
+255
在4个频道中
Get PRO
四月 '25
+266
在1个频道中
Get PRO
三月 '25
+285
在24个频道中
Get PRO
二月 '25
+449
在27个频道中
Get PRO
一月 '25
+380
在36个频道中
Get PRO
十二月 '24
+612
在53个频道中
Get PRO
十一月 '24
+576
在40个频道中
Get PRO
十月 '24
+439
在27个频道中
Get PRO
九月 '24
+470
在34个频道中
Get PRO
八月 '24
+466
在24个频道中
Get PRO
七月 '24
+458
在34个频道中
Get PRO
六月 '24
+431
在33个频道中
Get PRO
五月 '24
+442
在30个频道中
Get PRO
四月 '24
+371
在26个频道中
Get PRO
三月 '24
+374
在13个频道中
Get PRO
二月 '24
+846
在4个频道中
Get PRO
一月 '24
+590
在22个频道中
Get PRO
十二月 '23
+786
在34个频道中
Get PRO
十一月 '23
+431
在8个频道中
Get PRO
十月 '23
+522
在1个频道中
Get PRO
九月 '23
+842
在0个频道中
Get PRO
八月 '23
+1 001
在0个频道中
Get PRO
七月 '23
+1 476
在0个频道中
Get PRO
六月 '23
+1 096
在0个频道中
Get PRO
五月 '23
+916
在0个频道中
Get PRO
四月 '23
+908
在0个频道中
Get PRO
三月 '23
+878
在0个频道中
Get PRO
二月 '23
+584
在0个频道中
Get PRO
一月 '23
+1 454
在0个频道中
Get PRO
十二月 '22
+638
在0个频道中
Get PRO
十一月 '22
+1 656
在0个频道中
Get PRO
十月 '22
+536
在0个频道中
Get PRO
九月 '22
+537
在0个频道中
Get PRO
八月 '22
+857
在0个频道中
Get PRO
七月 '22
+1 735
在0个频道中
Get PRO
六月 '22
+523
在0个频道中
Get PRO
五月 '22
+218
在0个频道中
Get PRO
四月 '22
+268
在0个频道中
Get PRO
三月 '22
+186
在0个频道中
Get PRO
二月 '22
+424
在0个频道中
Get PRO
一月 '22
+734
在0个频道中
Get PRO
十二月 '21
+816
在0个频道中
Get PRO
十一月 '21
+663
在0个频道中
Get PRO
十月 '21
+1 410
在0个频道中
Get PRO
九月 '21
+565
在0个频道中
Get PRO
八月 '21
+1 525
在0个频道中
Get PRO
七月 '21
+1 531
在0个频道中
Get PRO
六月 '21
+1 533
在0个频道中
Get PRO
五月 '21
+2 084
在0个频道中
Get PRO
四月 '21
+1 811
在0个频道中
Get PRO
三月 '21
+8 672
在0个频道中
Get PRO
二月 '21
+1 828
在0个频道中
Get PRO
一月 '21
+1 309
在0个频道中
Get PRO
十二月 '20
+12 495
在0个频道中
日期
订阅者增长
提及
频道
02 七月0
01 七月0
频道帖子
Рефакторинг SSR: renderToString и renderToPipeableStream в React 19 Долгое время renderToString был монолитом: блокировал event loop до полного выплеска HTML, игнорируя Suspense-границы. Результат — пустая страница для пользователя, пока не загрузится всё. React 19 переосмысливает этот подход, добавляя гранулярное кэширование фрагментов и асинхронную потоковую отдачу. Как Suspense ломает статус-кво В React 19 renderToString научился работать с Suspense через механизм компенсированного выброса. Идея: React выдаёт готовый HTML, а то, что висит на Suspense, досылает через renderToPipeableStream. Это означает, что TTFB падает на 40-60%: не нужно ждать все данные сразу, браузер получает каркас страницы, а остальные куски с данными подгружаются постепенно. Гранулярное кэширование: не трогай статику Ключевая фича — кэширование отдельных Suspense-границ. Берёшь «шапку» и «блок комментариев» и кэшируешь их по отдельности. Если в комментариях данные устарели, шапка берётся из кэша без перерендера. Пример:
const fragmentCache = new Map();

function renderPage(req, res) {
  const stream = renderToPipeableStream(
    <Page userId={req.userId} />,
    {
      onShellReady() {
        res.write('');
        stream.pipe(res);
      },
      onFragmentComplete(id, html) {
        fragmentCache.set(id, html);
      }
    }
  );
}
Здесь onFragmentComplete даёт доступ к HTML каждого фрагмента по его id. Можно складывать в Map, Redis или CDN. Статичные блоки (шапка, подвал) перестают рендериться заново, что снижает нагрузку на сервер. Типичная ошибка: игнорировать React Flight Внутри React 19 renderToPipeableStream использует React Flight: режет HTML на чанки, каждый фрагмент — это свой Promise. Когда данные готовы, отправляется чанк. Старый renderToNodeStream выпилили. Ошибка — продолжать использовать renderToString для страниц с динамическими частями (лента, комментарии, графики). Это приводит к блокировке event loop и росту TTFB, особенно при высоком RPS. Практический совет: начинай с анализа Не беги внедрять гранулярное кэширование на каждую страницу. Для простых статичных страниц это оверинжиниринг. Но если у тебя продуктовая страница с частыми обновлениями (например, корзина или рекомендации), профит очевиден. Используй профилировщик React (например, React DevTools Profiler) и измеряй TTFB и TBT до и после. Оптимизируй только узкие места. Вывод: Гранулярное кэширование фрагментов в React 19 через Suspense и renderToPipeableStream — это production-ready паттерн для снижения TTFB и серверной нагрузки на динамических страницах, но требует вдумчивого выбора кандидатов для оптимизации.

2
📣 Появился новый шанс стать ИИ-специалистом и быстро найти работу МТС и НИУ ВШЭ объявили о старте набора на третий поток магистратуры «Исследования и предпринимательство в искусственном интеллекте». В программу включили 30 оплачиваемых мест от МТС и подготовку в области машинного обучения и ИИ. Для студентов добавили курсы по генеративному искусственному интеллекту, интеллектуальным агентным системам и проектированию ML-систем. Само обучение проходит на реальных кейса, а лучшие студенты смогут пройти стажировку или получить предложение о работе в МТС Web Services прямо во время обучения. Подать заявки на обучение можно по ссылке.
920
3
⁣Типизация императивных handlebars-шаблонизаторов в JSX через branded типы и кастомные JSX-фабрики Переход с Handlebars на React часто оставляет за собой строковые шаблоны с {{#each}} и {{#if}}. Прямая передача таких строк в JSX ломает TypeScript, но есть способ это контролировать на уровне типов. Branded типы как контракт данных Branded тип - это строка с уникальным символом, которая не может быть создана без специальной фабрики. Обычная строка не скомпилируется в такой тип, что предотвращает случайную передачу сырого шаблона. type HandlebarsTemplate = string & { __brand: 'Handlebars' }; function hbs(strings: TemplateStringsArray, ...values: any[]): HandlebarsTemplate { return strings.reduce((acc, str, i) => acc + str + (values[i] || ''), '') as HandlebarsTemplate; } function jsx(tag: string, props: { template?: HandlebarsTemplate } | null, ...children: any[]) { if (tag === 'Template' && props?.template) { return compileAndRender(props.template); } return React.createElement(tag, props, ...children); } Типичная ошибка и её устранение Если передать сырую строку напрямую, TypeScript выдаст ошибку: // ❌ Ошибка: Type 'string' is not assignable to type 'HandlebarsTemplate' <Template template="<div>{{user}}</div>" /> Корректный вариант с фабрикой: const template = hbs&lt;div&gt;{{#if user}}Hello {{user.name}}{{/if}}&lt;/div&gt;; function MyComponent() { return <Template template={template} />; } Production-oriented практика Чтобы кастомная JSX-фабрика работала, настрой Babel или TypeScript: добавь @jsx jsx в начале файла или укажи jsxFactory: 'jsx' в tsconfig. Для гибридных проектов это гарантирует, что ни один сырой шаблон не проскочит в рендер без компиляции. Если шаблон использует переменные контекста, добавь generics для типизации payload: function hbs<T>(strings: TemplateStringsArray, ...values: any[]): HandlebarsTemplate<T>; Trade-offs Дополнительный слой абстракции усложняет читаемость и увеличивает bundle, если шаблонов много. Используй такой подход только в пограничных сценариях миграции, а не как постоянную практику. Вывод: Branded типы и кастомные JSX-фабрики позволяют безопасно встраивать императивные шаблоны в декларативный JSX, предотвращая runtime-ошибки на этапе компиляции.
1 128
4
⁣Typed custom hooks: constraints и infer для строгих конфигов Часто вижу, как custom hooks принимают конфиги, где TypeScript выводит mode: string вместо 'edit' | 'view', а permissions — string[] вместо конкретных литералов. В production это ведет к потере автодополнения и багам на стороне потребителей. Constraints: фиксируем рамки Используй extends уже на уровне дженериков хука, чтобы сузить допустимые типы. Без него TS выводит широкие типы, с ним — точные литералы, если исходные данные переданы с as const. function useFeature<M extends string, P extends string[]>(config: { mode: M; permissions: P; }) { // TS: M, P как литералы } Infer: выводим сложные зависимости Когда конфиг содержит вложенные коллбэки или динамические ключи, infer в условных типах автоматически извлекает нужные типы из переданного объекта. Это спасает от ручного аннотирования. type ConfigResult<T> = T extends { mode: infer M; permissions: infer P } ? { mode: M; permissions: P } : never; Типичная ошибка: забытый as const Без as const TS выведет string, а не литералы. А если конфиг приходит из API — этот подход бесполезен. Зато для статически заданных конфигов в хуках (фичи, A/B-тесты, permission guards) это чистый профит: типы не протекают, код самодокументируется. Вывод: Constraints с extends и вывод через infer дают строгую типизацию factory-функций и конфигурационных объектов без потери читаемости и с проверками на уровне компиляции.
1 499
5
Попросили Claude создать WCAG-доступный DataPicker на React и потратили 3 дня на доработки Выбор даты кажется небольшой задач
Попросили Claude создать WCAG-доступный DataPicker на React и потратили 3 дня на доработки Выбор даты кажется небольшой задачей в UI, пока не попробуешь сделать его по-настоящему WCAG-доступным. Нам понадобился настраиваемый DataPicker на React для процесса записи на прием к врачу, где пользователи, работающие с keyboard navigation, и люди, использующие screen reader’ы, должны были выбрать дату без лишних затруднений. Claude сделал нам хорошую первую версию: структуру компонента, ARIA-атрибуты, базовую keyboard navigation и логику календаря. На первый взгляд результат выглядел почти готовым. Затем мы запустили NVDA, VoiceOver и протестировали сценарий keyboard navigation. Фокус выходил за пределы диалогового окна; некоторые даты озвучивались неверно; переключение между месяцами сопровождалось слишком тихим звуком; нажатие клавиши «Esc» закрывало календарь, но оставляло пользователя без контекста; режим высокой контрастности Windows нарушал отображение выбранного состояния. Код выглядел нормально, но UX оставлял желать лучшего. В этой статье мы рассмотрим реальную работу, стоящую за WCAG-доступным DataPicker'ом: где AI сэкономил нам время; где он не справился; как нам помог WAI-ARIA APG ; какие детали нам пришлось исправлять вручную и почему доступность нельзя проверить, просто прочитав сгенерированный код. Читать далее
1
6
Два способа создания доступного DatePicker'а с помощью AI: 80/20 или системное проектирование DatePicker потребовался на Reac
Два способа создания доступного DatePicker'а с помощью AI: 80/20 или системное проектирование DatePicker потребовался на React и TypeScript с корректной работой keyboard navigation, screen reader'а, управляемым состоянием и проверками доступности. Первый способ — дать AI четкий запрос, получить 80% кода, остальное доработать руками. Модель генерирует структуру календаря, атрибуты ARIA, базовую keyboard navigation и логику работы с датами. Затем начинаются проблемы: поведение фокуса становится нестабильным, возникают конфликты обработчиков событий, озвучивание screen reader'ами требует тестирования, небольшое изменение в логике может нарушить календарь. Второй способ — системное проектирование с AI-агентом. Включает PRD, декомпозицию задач, правила агента, внешнюю верификацию, Vitest, Playwright, сборку Vite, проверки типов и строгий цикл: агент не может двигаться дальше, пока не пройден текущий шаг. Далее разбирается, в чем AI действительно помог, где начал сбиваться, почему одного большого запроса недостаточно, как The Verifier изменил процесс и почему задача инженера сводится к контролю над замыслом, архитектурой, контрактами и стоимостью изменений. Читать далее на Habr
1 673
7
⁣React 19: ErrorBoundary с типизацией, error.cause и строгое логирование Границы ошибок в React долго выглядели как костыль для асинхронщины: раньше ErrorBoundary отлавливал только синхронные ошибки, а запросы с then/catch просто пролетали мимо. В 19 версии это починили через use и новые хуки, но типизация все равно остается местом, где можно наступить на грабли в production. Интерфейс и цепочка ошибок Интерфейс ErrorBoundaryState с Error | null, getDerivedStateFromError ловит ошибку, componentDidCatch отправляет в лог. Но что действительно спасает в реальных кейсах — это error.cause из ES2022. Когда fetch падает с 401, ты пробрасываешь не строку, а объект: * error.cause?.status для проверки статуса * error.cause?.message для текста interface AppError extends Error { cause?: { status: number; message: string; }; } Production: сужение и безопасность Передавать полный stack клиенту — плохая идея. Там пути файлов и внутренние адреса. Мы обрезаем первые 200 символов, если ошибка некастомная, и никогда не светим причину наружу: error.cause только во внутренний логгер. Схема логирования через instanceof: * если error instanceof AppError — пишем структурированно с типом, причиной и стеком * если просто Error — обрезаем стек, кидаем предупреждение * если вообще не Error — логируем как строку и показываем generic fallback Типичная ошибка instanceof не работает, если ошибка прилетела из другого iframe или realm. Там error.cause может быть сериализованным объектом, и придется проверять по полям вручную. Встречал такое при интеграции с микросервисными виджетами. Еще один момент — не засовывай всю логику в componentDidCatch: вынеси ее в отдельную функцию handleFatalError с возвратом never и переиспользуй, иначе при рефакторинге придется переписывать каждый Boundary. Вывод: Типизированный ErrorBoundary с error.cause и строгим логированием через instanceof — это надежный паттерн для обработки асинхронных ошибок, который дает консистентность и безопасность в production. Источники: React docs про Error Boundaries, MDN про Error.cause, React 19 release notes про use() hook и async errors.
1 802
8
⁣Типизация ErrorBoundary в React 19: асинхронные ошибки, error.cause и production-логирование ErrorBoundary долго был слабым звеном — он не ловил ошибки из промисов, useEffect или setTimeout. React 19 не меняет классовый подход, но появление error.cause и правильная типизация через instanceof делают обработку асинхронных ошибок предсказуемой и безопасной. Почему any в state — ошибка Состояние границы должно быть строго типизировано: Error | null. В getDerivedStateFromError проверяй cause через instanceof Error, не через truthy check. Это исключает мусор из продакшена, где cause может быть строкой, числом или undefined. Production-кейс с fetch При HTTP 500 выбрасывай: throw new Error('HTTP error', { cause: res.status }) В границе проверяй: if (error.cause instanceof Error) { // Логируем полный стек } else if (typeof error.cause === 'number') { // Игнорируем, если статус < 500 } Это позволяет не слать в Sentry временные сетевые сбои. Типизация cause и instanceof Используй instanceof для разграничения классов ошибок — HTTPError, NetworkError, ValidationError. Так ты контролируешь, что идёт в Sentry, а что — только в консоль. Без этого логгер захлебнётся шумом. Вывод: Типизированный ErrorBoundary с error.cause и instanceof превращает асинхронные ошибки из невидимых багов в управляемые инциденты.
1 799
9
⁣Как вывести типы контекстов во вложенных layout без ручных интерфейсов в React Router v7 Если вы используете TanStack Router (React Router v7) с вложенными layout, то наверняка сталкивались с ситуацией, когда родительский layout прокидывает loader-контекст, а дочерний компонент вынужден вручную объявлять интерфейс и кастить. Типовая ошибка — забыть обновить интерфейс при изменении структуры данных, что ведет к багам на проде. Как работает типовая цепочка Система типов строится вокруг дженериков createRoute. Каждый роут наследует контекст от родителя через getParentRoute(). TypeScript автоматически мержит результаты loader родительского и текущего роута: const rootRoute = createRootRoute()({ loader: () => ({ user: 'John' }) }); const layoutRoute = createRoute({ getParentRoute: () => rootRoute, loader: ({ context }) => ({ theme: context.user === 'John' ? 'dark' : 'light' }) }); const pageRoute = createRoute({ getParentRoute: () => layoutRoute, loader: ({ context }) => { // context: { user: string, theme: string } return context.theme; } }); Ни одного явного интерфейса или каста. Система выводит тип контекста из цепочки родительских роутов. Production-кейс: аутентификация с правами Практический пример: вложенный layout с авторизацией. Родитель кладет user, дочерний — permissions: const authLayout = createRoute({ getParentRoute: () => rootRoute, loader: () => ({ user: fetchUser() }) }); const adminLayout = createRoute({ getParentRoute: () => authLayout, loader: ({ context }) => ({ permissions: fetchPermissions(context.user.id) }) }); Страница получает { user, permissions } без ручного объявления. Если переименовать user в currentUser — TypeScript подсветит все места, где используется старый ключ. Типичная ошибка и trade-off Ошибка: ручное объявление interface IContext = { user: string } и последующий as any при несоответствии. Это ломает всю type-safety и ведет к runtime-ошибкам при изменении данных. Trade-off: полагаться на вывод типов чуть сложнее читать в IDE, чем явные интерфейсы, но выигрыш в надежности при рефакторинге — код сам документирует контракты, и компилятор ловит несоответствия. Вывод: Используйте автоматический вывод типов контекстов через цепочку getParentRoute, чтобы избавиться от ручных интерфейсов и кастингов, повысив надежность и упростив рефакторинг в production-коде.
1 737
10
⁣Signal vs Observable: почему ваш граф зависимостей взрывается в production Когда в корзине интернет-магазина total = price * qty, затем скидка от суммы, затем налог — Observable (RxJS) на каждое изменение price пересчитывает все подписки, даже если данные не изменились. Разработчики навешивают distinctUntilChanged на каждую цепочку, и через месяц граф превращается в лапшу. Как работают Signal Signal (SolidJS, Vue 3.4+, Preact Signals, Svelte 5) делает вычисления ленивыми. Он отслеживает, какие сигналы реально читаются в computed, а не подписывается на всё подряд. Два сигнала, зависящих от одного источника, не подписываются дважды. При изменении источника пересчитываются только те, кто на него ссылается, а не всё дерево. Пример на псевдокоде Solid: count = signal(1) price = signal(100) total = computed(() => count() * price()) discount = computed(() => total() > 500 ? 0.1 : 0) final = computed(() => total() * (1 - discount())) При изменении count пересчитываются total и final. Discount пересчитывается только если total пересечет порог. В RxJS через combineLatest discount будет пересчитываться на каждое изменение count и price, даже если total не изменился. Меньше кода — больше бесполезной работы. Когда Observable все еще нужен Observable (RxJS) хорош для асинхронных цепочек: debounce, switchMap, WebSocket. Это декларативные потоки, где важны трансформации во времени. Но при сотне зависимостей начинается цирк: каждый чих триггерит пересчёт, дебажить dependency hell — отдельный квест. Типичная ошибка — использовать Observable для синхронного графа зависимостей, где сигналы дают ровно то же самое без лишних пересчётов. Практический совет Если у вас в проекте уже RxJS — не переписывайте всё. Для нового фича с interdependent состояниями (формы, редакторы, дашборды) закладывайтесь на сигналы. В продакшене с сотнями зависимостей сигналы дают на 30-50% меньше бесполезных перерисовок и упрощают отладку: граф зависимостей прозрачен, а не взрывается на каждой итерации. Вывод: Signal — для синхронных графов с предсказуемыми изменениями, Observable — для event-driven асинхронных потоков, и выбор между ними — это trade-off между производительностью вычислений и гибкостью трансформаций.
1 791
11
🤣 Общаться о синтаксисе теперь тоже можно через агентов ✖️ xCode Journal
🤣 Общаться о синтаксисе теперь тоже можно через агентов ✖️ xCode Journal
1 838
12
⁣requestAnimationFrame и React-батчинг: почему дёргается drag-and-drop Сделал перетаскивание элементов в production? На быстрых движениях начинаются дёргания и выпадающие кадры. Проблема знакомая: requestAnimationFrame вызывается до того, как React применяет батч из setState. Особенно если rAF висит на mousemove вне реактовских обработчиков. В React 18+ батчинг работает в рамках одного события, но rAF срабатывает до мерджа обновлений. Результат: анимация читает старую позицию, React применяет новые значения с задержкой, элемент дёргается. Решение с flushSync Для чтения DOM-значений (позиция, размер) используй useLayoutEffect — выполняется до показа кадра. Внутри drag-логики добавь синхронный вызов: const handleMouseMove = (e: MouseEvent) => { ReactDOM.flushSync(() => { setPos((prev) => ({ x: prev.x + e.movementX, y: prev.y + e.movementY })); }); requestAnimationFrame(updateStyles); }; flushSync заставляет React применить батч синхронно перед следующим кадром. Дёргания уходят. Предупреждение Не злоупотребляй flushSync — он ломает Concurrent Mode и режет производительность, если вызывать на каждом mousemove. Типичная ошибка: считать его универсальным решением без оценки trade-offs. Альтернативы - Замени mousemove/touchmove на pointer-events — они синхроннее в браузере. - Если хочешь не трогать React-состояние внутри кадра, храни позиции в useRef и обновляй DOM руками через rAF. Но это редкий кейс, и он разрушает реактивность. Вывод: requestAnimationFrame и React-батчинг конфликтуют по умолчанию — контролируй синхронизацию через flushSync или выноси анимации за пределы реактовского цикла, иначе даже гладкий код споткнётся на быстром перемещении.
1 881
13
⁣Почему useSyncExternalStore проигрывает Zustand и Jotai на композитных сторах и как селекторы убивают производительность Пытался вчера прикрутить useSyncExternalStore к композитному стору в React 19. Думал, что если это API из коробки, то и проблем с производительностью не будет. На деле — пришлось переписывать половину. Бенчмарк на 1000 подписчиков: примитивы vs вложенные объекты На простом сторе (один селектор, один примитив) разница в микросекундах — Jotai ~0.15ms, Zustand ~0.18ms, useSyncExternalStore ~0.22ms. Можно не париться. Как только в сторе появляется вложенный объект — начинается треш. Zustand без shallow перерисовывает всех подписчиков при любом изменении. Даже если твой селектор брал только state.user, а кто-то обновил state.posts. Причина: Zustand при каждом вызове селектора сравнивает результат через Object.is с предыдущим. Если селектор возвращает новый объект — привет, ререндер. Jotai с производными атомами — та же петрушка. atom(get => { return { user: get(userAtom), posts: get(postsAtom) } }) — создаёт новый объект на каждый вызов. Jotai также сравнивает через Object.is, так что ререндер гарантирован. Грабли с селекторами: деструктуризация убивает производительность Ошибка, которую я вижу на каждом проекте — деструктуризация в теле селектора: const { user, posts } = useStore(state => ({ user: state.user, posts: state.posts })) Каждый вызов селектора создаёт новый объект. Object.is никогда не вернёт true. Правильный подход — писать селекторы на отдельные поля: const user = useStore(state => state.user) const posts = useStore(state => state.posts) Тогда Object.is вернёт true, если ссылка не изменилась. Дополнительные меры: Zustand — используйте shallow для сравнения. Jotai — atomWithComparator. Экономия до 40% ререндеров. Когда useSyncExternalStore — зло для бизнес-логики useSyncExternalStore вообще не про бизнес-логику. Его пилили под внешние источники: WebSocket, IndexedDB. Внутри React он не батчит подписки, так что при 1000 подписчиков и частых изменениях — 1.2ms на ререндер. Jotai с мемоизацией — 0.3ms. Zustand с shallow — 0.6ms. Разница в 4 раза — это уже не погрешность. Сейчас на проекте простая архитектура: простые состояния — Zustand, меньше кода. Композитные данные — Jotai с atomFamily и splitAtom. useSyncExternalStore — только если прикручиваешь что-то внешнее. Вывод: Микрооптимизация селекторов и отказ от композитных возвращаемых объектов — это не premature optimization, а разница между 1.2ms и 0.3ms ререндера при масштабировании на 1000+ подписчиков.
1 814
14
⁣Типизация WebSocket событий: как discriminated unions + brand checks делают стейт-машину предсказуемой и спасают от багов с payload Каждый, кто писал real-time фичи на React или Vue, сталкивался: диспатчишь событие WebSocket, а payload — не того типа. Структура похожа, семантика разная, и TypeScript молчит. Ошибка уходит в production. Discriminated unions — база, но не панацея Стандартный подход: поле type сужает остальные поля. Но если два события содержат sessionId с разным смыслом, TS не заметит подмены. На уровне типов всё ок, в рантайме — треш. Brand checks — второй рубеж Добавьте фиктивное поле __brand: 'open' в payload. В рантайме его нет, типы не гоняют лишние данные. Компилятор не даст смешать payload разных событий. Это имитация номинативной типизации в структурной системе TypeScript. Стейт-машина перестаёт принимать невесть что: переходы становятся предсказуемыми. Пример типизации: type WSMessage = | { type: 'OPEN'; payload: { sessionId: string } & { __brand: 'open' } } | { type: 'MSG'; payload: { userId: number; text: string } & { __brand: 'msg' } } | { type: 'ERROR'; payload: { error: string } & { __brand: 'err' } }; Типичная ошибка Думать, что brand checks — это всё. WebSocket может прислать что угодно. Типы не панацея. В production обязательно докидывайте валидацию схемы на каждое входящее сообщение через Zod или io-ts. Практический совет Используйте brand checks как инструмент для разработчика, чтобы не протащить payload не туда при сборке стейта. А рантайм-валидацию — как первую линию защиты от кривых данных с сервера. Вместе они дают надёжность и предсказуемость в real-time архитектуре. Вывод: Discriminated unions сужают типы по значению, brand checks защищают от семантических багов, а рантайм-валидация — от любых неожиданностей с сервера: три уровня защиты для production-grade стейт-машины.
2 035
15
⁣Реактивный DataV: оптимизация 10k+ точек с event-driven аннотациями и hit-тестингом Когда на графике 10 000+ точек, часто выбирают: производительность или интерактивность. Прямой рендер в SVG тормозит, Canvas летает, но теряет hover и click. Это ложный компромисс. Гибридный подход Решение — Canvas для рендера точек (никаких DOM-узлов, только пиксели). Hover и click выносятся в event-driven аннотации через raycasting. Считай только при движении мыши, а не на каждый фрейм. Ленивые аннотации и кеширование Рендери только те аннотации, которые попадают в viewport, и кешируй результат. Иначе каждый чих дергает рендер на 10k точках. Пример hit-тестинга Без фреймворков, на TypeScript: class ScatterPlot { handleMouseMove(e) { for (const p of this.points) { if (Math.abs(mouseX - p.x) <= p.r * 2 && Math.abs(mouseY - p.y) <= p.r * 2) { if (Math.hypot(mouseX - p.x, mouseY - p.y) <= p.r) { this.showAnnotation(i); return; } } } } showAnnotation(index) { requestAnimationFrame(() => this.renderAnnotationLayer()); } } Сначала проверка по bounding box, потом по расстоянию. Для 10k точек O(n) срабатывает за пару миллисекунд. QuadTree даёт O(log n), но не всегда нужен. Предупреждение о типичной ошибке Главный подвох: не забудь сбросить аннотацию при уходе мыши с точки. Иначе график зависнет с подписями на все 10k, и пользователь решит, что интерфейс сломался. Вывод: Event-driven аннотации с hit-тестингом — это trade-off между точностью интерактивности и производительностью рендера, где Canvas даёт скорость, а raycasting сохраняет UX.
2 134
16
🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы Внутри статьи она подробно расписывает этапы собес
🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы Внутри статьи она подробно расписывает этапы собеседований, лайфхаки и делится учебными ресурсами, которые ей помогли. Плюс девушка великодушно оставила ссылки на свой Notion с полезными заметками по математике и LLM. ✖️ xCode Journal
2 452
17
⁣Мемоизация в React 19: когда useMemo и memo уже не панацея, а новые use() и React Forget меняют правила игры Сколько раз вы писали useMemo(() => expensiveComputation(a, b), [a, b]) и сомневались, нужно ли это вообще? Или оборачивали компонент в memo, надеясь, что поверхностное сравнение пропсов не окажется дороже самого рендера? В React 19 старые подходы перестают быть единственно верными: появились инструменты, которые делают ручную мемоизацию узким, а не основным решением. Почему старые инструменты не идеальны Мемоизация не бесплатна. useMemo хранит ссылки и пересчитывает зависимости при каждом рендере. memo добавляет сравнение пропсов, и если дерево сложное, а данные меняются часто, выигрыша нет. В production я видел проекты, где 80% вызовов useMemo и useCallback не давали прироста — это была оптимизация ради оптимизации без профилирования. Типичная ошибка: мемоизировать все подряд, а не только горячие пути. Новый хук use() — отказ от явного кеширования промисов Хук use() компилируется в синхронный рендер и убирает необходимость вручную кешировать асинхронные данные. Пример: вместо того чтобы писать useEffect с загрузкой и оборачивать результат в useMemo, вы просто используете use(fetchUsers()). Компилятор сам решает, когда перерендерить компонент, без лишних проверок и ссылочных сравнений. Это особенно полезно для SSR и Suspense — мемоизация тут не нужна, рендер планируется автоматически. React Forget — компилятор как замена ручной оптимизации React Forget анализирует граф вызовов и сам встраивает кеширование. Если функция вызывается с теми же аргументами, компилятор возвращает прошлое значение, причём без утечек памяти. Разработчику не надо думать, где ставить useMemo — это происходит на уровне анализа зависимостей. Практический совет: используйте это на побочных проектах уже сейчас, но не спешите переписывать всё в проде. Для 95% сценариев React Forget достаточно, но на очень горячих путях — например, рендер таблицы на 10 тысяч строк — ручной useMemo всё ещё может быть оправдан. Типичная ошибка: мемоизация как архитектурный костыль Если ваш код на 80% состоит из useMemo и useCallback, это повод пересмотреть архитектуру, а не добавлять новые обёртки. use() берёт на себя загрузку данных, Suspense — ленивую подгрузку, а React Forget — всё остальное. Главный trade-off: экономия времени на ручной мемоизации против возможных дополнительных рендеров на этапе доработки компилятора. В production это редко становится проблемой, если дерево компонентов спроектировано с учётом React Forget. Вывод: Будущее за компиляторами, которые избавляют от рутины — useMemo и memo остаются инструментами для узких горячих путей, а не ежедневной практикой.
2 483
18
⁣Object.assign() и structuredClone() в React стейте: баги с прототипами и Date При immutable-обновлении вложенных объектов в React стейте разработчики часто полагаются на поверхностное копирование через spread или Object.assign(), забывая, что эти методы не сохраняют прототипы и сериализуют Date в строку. Это приводит к тихим багам, которые проявляются только в production при работе с классами или сложными типами. Баг с прототипами: Object.assign() обнуляет методы класса Когда объект содержит экземпляр класса, Object.assign({}, obj) копирует только собственные свойства, игнорируя прототип. Например, методы полностью теряются: class User { constructor(name) { this.name = name; } greet() { return Hi, ${this.name}; } } const original = new User('Alice'); const copy = Object.assign({}, original); console.log(copy.greet()); // TypeError: copy.greet is not a function structuredClone() решает эту проблему для сериализуемых типов, но классы с методами не поддерживаются — код упадет с DOMException. Решение: если в стейте лежат инстансы классов, используйте кастомную функцию глубокого копирования с проверкой прототипа через Object.getPrototypeOf. Баг с Date: structuredClone() превращает дату в строку structuredClone() сериализует Date в строку, как JSON.stringify. После клонирования методы Date (getTime, toISOString) перестают работать: const state = { date: new Date('2024-12-25') }; const clone = structuredClone(state); console.log(clone.date.getTime()); // TypeError: clone.date.getTime is not a function В React стейте это особенно опасно для UI-компонентов, которые полагаются на методы Date (например, календари или таймеры). Типичная ошибка: разработчики думают, что structuredClone() копирует Date нативно, но по спецификации оно заменяет Date на строку UTC. Совет: используйте structuredClone только для данных, которые проходят полный список сериализуемых типов (Map, Set, ArrayBuffer), а для Date заводите отдельный хелпер или библиотеку вроде Immer. Trade-off: поверхностное vs глубокое копирование - Поверхностное копирование (spread, Object.assign) быстрое и простое, но не подходит для вложенных структур с прототипами или ссылочными типами (Date, Map). Риск: мутация общего стора через ссылки. - Глубокое копирование (structuredClone, Immer) безопаснее, но медленнее и несовместимо с некоторыми типами. Практический совет: для production используйте Immer, который корректно работает с прототипами и Date, а structuredClone применяйте только для изолированных данных (cookies, clipboard). Вывод: immutable-обновление в React требует осознанного выбора метода копирования в зависимости от типов данных в стейте, а не слепого использования spread "на всякий случай".
2 210
19
😁 Пункта про стоимость и требуемые характеристики к железу не хватает ✖️ xCode Journal
😁 Пункта про стоимость и требуемые характеристики к железу не хватает ✖️ xCode Journal
2 243
20
⁣Глубокая типизация GraphQL-ответов без codegen: generics и conditional types в TypeScript Codegen для GraphQL часто превращается в головную боль: настройка схемы, синхронизация версий, регенерация при каждом изменении. Но если вы используете Relay с фрагментами или работаете в микросервисе с часто меняющейся схемой, можно обойтись без codegen. Типичная ошибка — вручную писать типы для каждого запроса, что для вложенных структур с пагинацией превращается в копипасту. Почему codegen не всегда нужен Codegen добавляет слой абстракции, который требует CI/CD-интеграции и синхронизации с серверной схемой. В проектах с частыми изменениями или фрагментарной архитектурой Relay это может стать узким местом. Альтернатива — вывести типы из самого запроса через TypeScript. Подход: generic + conditional type Создаем маппер типов GraphQL на TypeScript и utility type, который рекурсивно "проходит" по структуре запроса, обнаруженной через as const. Этот тип заменяет строки на реальные типы из схемы, обрабатывая вложенность и массивы. // Маппер типов схемы type GraphQLTypeMap = { ID: string; String: string; User: { id: string; name: string; posts: Post[] }; Post: { id: string; title: string; comments: Comment[] }; Comment: { id: string; text: string; author: User }; }; // Рекурсивный conditional type type UnwrapGraphQLResponse<T> = T extends { __typename: infer Name } ? T & GraphQLTypeMap[Name extends keyof GraphQLTypeMap ? Name : never] : T extends (infer U)[] ? UnwrapGraphQLResponse<U>[] : T; // Запрос с as const const query = { user: { id: true, name: true, posts: { title: true, comments: { text: true, author: { name: true } } } } } as const; // Тип ответа выводится автоматически type Response = UnwrapGraphQLResponse<typeof query>; Акцент на trade-offs Главный плюс — один дженерик на все запросы без codegen. Но это требует ручного поддержания GraphQLTypeMap, что для схем с 50+ типами становится трудоемким. Для union/interface типов conditional type может дать сбой — понадобится доплогика. Также метод не работает с динамическими selection set. Типичные ошибки * Забыть указать as const — без него TypeScript не зафиксирует структуру запроса. * Использовать при сложных интерфейсах: conditional type не всегда корректно обрабатывает discriminated unions — это потребует дополнительных проверок. * Не синхронизировать маппер: если схема меняется, типы рассинхронизируются, и вы получите ложные гарантии. Практический совет Применяйте этот подход для микросервисов с простой, стабильной схемой или когда codegen конфликтует с Relay-фрагментарной структурой. В production я так типизировал запросы в проекте, где схема менялась раз в неделю, а codegen падал из-за кэширования — это сэкономило время без потери безопасности типов. Вывод: Generics и conditional types — это легковесная альтернатива codegen для типизации вложенных GraphQL-ответов, но она требует ручного управления маппером и подходит для схем ограниченного размера.
2 145