SuperOleg dev notes @super_oleg_dev Channel on Telegram

SuperOleg dev notes

@super_oleg_dev


Обзоры новостей и статей из мира frontend, интересные кейсы, исследования и мысли вслух

https://github.com/SuperOleg39

https://twitter.com/ODrapeza

@SuperOleg39

SuperOleg dev notes (Russian)

SuperOleg dev notes - это канал, посвященный обзорам новостей и статей из мира frontend. Здесь вы найдете интересные кейсы, исследования и мысли ведущего разработчика - Супер Олега. Он делится своим опытом, знаниями и уникальными подходами к разработке веб-приложений. Если вам интересны последние тенденции в сфере frontend, то этот канал станет вашим незаменимым источником информации. Присоединяйтесь к нам, чтобы быть в курсе всех новостей и разработок в мире веб-технологий! Для доступа к дополнительным материалам и контактам, посетите наш GitHub профиль по ссылке: https://github.com/SuperOleg39 и следите за нами в Twitter: https://twitter.com/ODrapeza Мы также доступны по юзернейму @SuperOleg39. Присоединяйтесь к нам и узнавайте первыми обо всех новинках искусства разработки веб-приложений!

SuperOleg dev notes

14 Nov, 11:46


Следующая проблема - управление памятью.

Просто так выделить фиксированный X gb памяти на ноду - не вариант, оперативка в облаке это один из самых дорогих ресурсов.

Только появление swap-space в k8s позволило решить проблему перенаправления памяти между подами, потому что ранее это означало необходимость прибивать процессы.

В целом, поддержка memory swap практически убрала необходимость в перенаправлении выделенной памяти. Swap (или файл подкачки) - позволяет не активные данные сбросить из оперативки на диск, и обратно.

Далее, проблема - оптимизация производительности хранилища (I/O операции)

При работе с кэшами, бэкапами, большими образами, скорость чтения и записи очень влияет на производительность окружения.

Тут рассказывают про баланс между стоимость, скоростью и надежностью и какие решения смотрели (из всего знаком только с s3):
- SSD RAID 0 - очень быстро, но привязано к конкретной ноде, выйдет из строя конкретный диск - все данные потеряны. Этот подход используют на данный момент, таких инциндентов не было.
- Block Storage - виды хранилищ который привязаны к нодам, те же проблемы с потерей данных, медленнее, но широко распространены
- Persistent Volume Claims - k8s абстракция поверх реального хранилища в кластере, claim - по сути запрос на конкретный размер диска, нужные права и прочее. Гибко, но есть проблемы со временем привязки на старте, надежностью, и другие ограничения.

Бэкапы и восстановление дисков очень затратная операция, тут выбрано интересное решение:
- архивы заливаются в s3 и скачиваются оттуда
- архивы не сжатые
- дело в том что обычно упираются в CPU, а не в пропускную способность сети, и не выгодно сжимать и распаковывать эти архивы (но тут подчеркивают что важен баланс)

Еще один интересный кейс с I/O операциями - доступная пропускная способность на чтение и запись шарится между воркспейсами.
Пока не начали ограничивать для каждого по отдельности, каким-то воркспейсам постоянно не хватало ресурса.

Используют похоже какой-то кастомный механизм - лимитер на основе cgroups2, нагуглил пример - https://andrestc.com/post/cgroups-io/

SuperOleg dev notes

14 Nov, 09:01


Привет!

Попалась очень интересная статья от Gitpod, про их инфраструктуру для development окружений, и как они ушли от Kubernetes к кастомному решению.

Главный минус статьи, что непосредственно про нюансы реализации кастомного решения мало информации, и только по кускам можно получить в следующих статьях в блоге.

В целом, много тем от которых я далек, и погружен не сильно, объяснить не смогу и оставлю ссылки из статьи.

Но всегда были интересно как устроены песочницы, такие как Codesandbox и Stackblitz, в блоге которых тоже попадаются классные статьи, иногда про них пишу - https://t.me/super_oleg_dev/141, и решаемые проблемы в статье Gitpod во многом с ними пересекаются.

Итак, Gitpod это классное облачное решение для разработки.

Такое development окружение имеет ряд особенностей:
- наличие состояния, которое так просто с одной ноды на другую не перенести - исходники, собранный код, кэши и так далее
- вообще не вариант терять изменения в исходном коде при разработке
- непредсказуемое потребление ресурсов, например пиковые потребления на сборку, и минимальные в остальное время
- безопасность, зачастую разработчикам нужен root доступ на ноде (вплоть до возможности развернуть свой Docker и k8s, да, внутри докера и k8s :crazy: ), это не должно аффектить другие нодыв кластере

Kubernetes был выбран для инфраструктуры Gitpod как очевидное решение, но столкнулись с рядом сложностей, и трудности в управлении и поддержки при масштабировании, и то что k8s заточен под контролируемые окружения и нагрузки, далее подробно разбираются отдельные кейсы.

Первая проблема - управление ресурсами, а именно CPU, память и сеть (пропускная способность).

Пиковые нагрузки на CPU при разработке, важность отзывчивости интерфейсов редактора и терминала, каким-то образом надо выделять не слишком мало ресурсов, и не хотим что бы простаивали значительные.

Пробовали схемы с Completely Fair Scheduler (CFS), но он не предсказывает потребление, а увеличивает ресурсы когда их не хватает - то есть уже слишком поздно.

Если выделять статичные ресурсы, тут тоже проблема, разные процессы (даже VS Code запустит пачку процессов) в итоге могут конкурировать и CPU так же будет не хватать.

Пробовали приоритезацию процессов, но и там свои трудности, связанные с реализацией механизма.

В итоге все замиксовали и остановились на решении с динамическим выделением ресурсов (появилось в k8s) + CFS + приоритеты процессов основанные на cgroupsv2.

SuperOleg dev notes

12 Nov, 15:02


Привет!

Небольшой но интересный баг с микрофронтами, вебпаком и loadable.

Ранее я уже писал про интеграцию loadable для создания многостраничных микрофронтов Child Apps с разделением кода - https://t.me/super_oleg_dev/183

Вебпак собирает отдельные модуля в чанки, внутри они хранятся в мапе вида:

{
1234: function(...) { исходный код, экспорты/импорты },
5678: function(...) { исходный код, экспорты/импорты },
...
}


Где цифры - уникальные id этих модулей.
Модули из разных чанков после загрузки затем попадают в общую мапу, из которой вебпак будет доставать их при импорте.

Исследуя ошибку, увидел что в экспорте микрофронта получаю объект с переменными из нашего UI-kit.

Сначала грешил на Module Federation, так или иначе все стектрейсы проходят через него.

Но в итоге увидел, что вебпак ID для модуля микрофронта Foo в его чанке такой же, как ID модуля переменных UI-kit, внимание, в чанке другого микрофронта Bar.

Bar загружается раньше, его модуль попадает в общую мапу, и далее при импорте Foo, по указанному ID мы получаем эти переменные вместо микрофронта.

Проблема конечно же в глобальной мапе, в нашем случае это переменная:
window.__LOADABLE_LOADED_CHUNKS__


В которую попадает тысячи модулей.
А учитывая независимые сборки микрофронтов, попасть на такую коллизию было просто делом времени.

Оказывается, вебпак плагин Loadable переопределяет эту переменную для сборки.

Хорошо что есть из коробки возможность переопределить эту переменную, сделал уникальной для каждого микрофронта:
{
chunkLoadingGlobal: `__LOADABLE_LOADED_CHUNKS__child_${name}_${version}__`
}

Проверил разные кейсы, шаринг через Module Federation не пострадал, ошибка ушла.

Потом собрал без Loadable плагина, увидел что создается уникальное название, для моего чанка - webpackChunkchild_app_state_0_3_18, что можно увидеть и в дефолтах вебпака.

То есть проблема появилась только при интеграции нового функционала.

Пример такой проблемы, о которой даже знать и думать не будешь, пока не столкнешься.

И тот случай где ну очень удобно делать отладку через Chrome Overrides, без необходимости публиковать обновленный код пачки микрофронтов.

SuperOleg dev notes

29 Aug, 08:23


Крутой внутренний продукт вышел на публику - https://t.me/unidrawio

Пользуюсь регулярно, в том числе проектировал в нем Microfrontends Platform (о котором уже рассказывал в этом канале), приятно порекламировать.

SuperOleg dev notes

28 Aug, 11:01


Возможно будет история еще об одной утечке, связанной с Async Local Storage, но как минимум хочу рассказать про интересный кейс, связанный с отладкой этой проблемы.

Мы используем Fastify в качестве веб-сервера для Tramvai, разбираясь с утечкой поставил логи на хуки onRequest и onResponse.

Даю нагрузку на приложение через autocannon, обнаружил что onResponse логов меньше чем onRequest, хотя ответы от приложения приходят все.

Хорошо (хоть и поздно) заметил что не хватает ровно столько onResponse сколько запросов к параллельному вызову я указал для autocannon, условные 10 последних из 100 отправленных.

Похоже, autocannon не дожидается последнюю пачку запросов до конца, и прерывает их до того как стрим ответа будет завершен.

Обнаружил это через подписку на события finish и close объекта Request - оказалос что finish так же не вызывается эти 10 раз, а последние 10 close прилетают пачкой одновременно.

app.addHook('onRequest', async (request, reply) => {
request.raw.on('finish', () => { ... });
request.raw.on('close', () => { ... });
})


Понаставил логов в fastify, посмотрел исходники, и оказывается хук onResponse вызывается как раз на событие finish.

И это получается дефолтное поведение в Node.js, если Request был отменен клиентом (request.aborted), событие finish не срабатывает, даже когда reply.send(...) будет вызван после отмены и фактически завершит стрим.

Это кстати можно мониторить, есть гайд у fastify - https://fastify.dev/docs/latest/Guides/Detecting-When-Clients-Abort/#detecting-when-clients-abort

Вот такой вот случайно обнаруженный сайд-эффект из-за нюансов работы autocannon, который в теории может воспроизводиться и на продакшене, и получается onResponse не самый надежный вариант для очистки чего-либо после завершения запроса.

SuperOleg dev notes

27 Aug, 20:27


Итак, а что же утекает?

Дело в том, что для предотвращения лишних запросов за серверным кодом микрофронтов и экономии на парсинге строки в код, результаты работы этого загрузчика кэшируются в LRU-кэше.

И вот эта маленькая и безобидная стрелочная функция со всем своим богатым Closure сохраняется в памяти приложения практически на все время его жизни.

В приложениях могут быть сотни страниц, на которых используются десятки разных микрофронтов.

А в Tramvai есть модуль прогрева кэшей, который на старте сервера делает запросы к каждому роуту приложения.

Таким образом сразу после релиза, кэши прогреты, приложения работают быстрее, но и потребление памяти растет сразу при наличии утечки.

Интересно, что у нас есть тест на утечки памяти который недавно актуализировали - но такую комбинацию модулей и большого количества страниц и микрофронтов как у проблемного приложения мы вряд ли воспроизведем в этом тесте.

Также, не до конца понятно на каких уровнях надо чинить утечку.

С одной стороны не хочется трястись над каждым замыканием и бояться создавать новые функции (а как мы видим это и тулинг может сделать без нашего участия).

С другой стороны хочется предотвратить возможность выстрела в ногу, и исправить такие места как createCache с ссылкой на commandLineExecutionContext.

В общем есть о чем подумать, интересны ваши мысли и опыт исправления таких вещей.

SuperOleg dev notes

27 Aug, 20:15


Причина просто прекрасна - на этапе сборке, судя по всему этим занимается именно Terser, декларации функций перемещаются в место их использования.

На примере выше, код превращается примерно в такой:

function getMM({ httpClient }) {  
...
var compiled = function stringToObject(data) {
// а вот и httpClient в замыкании :)
...
}(data)
...
}


Дальше разберем как эта ссылка утекает в код микрофронта.

Код в методе stringToObject вызывает vm.runInThisContext, которая нам уже отдает все что экспортирует код микрофронта, который в свою очередь экспортируем специальную фабрику.

Эту фабрику мы тут же вызываем с необходимыми аргументами, один из которых функция из этого же файла, условно:

const customRequire = (...) => { ... };

function getMM({ httpClient }) {
...
var compiled = function stringToObject(data) {
return vm.runInThisContext(data)(..., customRequire, ...)
}(data)
...
}


Конечно же, объявление функции customRequire переместилось и превратилось в анонимную функцию по месту использования:

function getMM({ httpClient }) {  
...
var compiled = function stringToObject(data) {
return vm.runInThisContext(data)(..., (...) => { /* а вот и замыкание для нашего httpClient! */ }, ...)
}(data)
...
}


На скриншоте оригинальный собранный код, только после форматирования в профайлере.

SuperOleg dev notes

27 Aug, 19:59


Идем дальше.

До утечки не было никаких криминальных изменений, в профайлере видно, что не очищается ссылка на трамвайный HTTP клиент, который теперь стал использоваться в модулях, которые занимаются всем вокруг загрузки и выполнения кода микрофронтов.

Но по цепочке видно, а также по приложенному стектрейсу, что утечка начинается изнутри кода микрофронтов, которые мы выполняем в изолированном контексте!

Начнем с функции - загрузчика getMM - HttpClient передается туда явно, из приложения, присутствие объекта в замыкании ожидаемо.

Максимально упрощенный код:

const getMM = ({ httpClient }) => {
...
}


Дальше, уже странность. Есть отдельный метод, он используется внутри функции загрузчика, но в его замыкании тоже есть ссылка на httpClient:

js 
const stringToObject = (data) => {
// именно тут в closure вижу httpClient
...
}

const getMM = ({ httpClient }) => {
...
stringToObject(data)
...
}


Затем есть вообще анонимная функция, которая также в замыкании содержит ссылку на httpClient, и именно она выполняется внутри кода микрофронта.

SuperOleg dev notes

27 Aug, 19:45


Раскручиваем начиная с конца, и это у нас - HttpClient.

В коде HTTP клиента есть безобидная строчка - создается стрелочная функция, если упростить:

createCache: createCache ? (cacheOptions) => createCache('memory', cacheOptions) : undefined,


И тут наше первое замыкание, которое еще само по себе не проблема.

Эта функция создается в контексте фабрики HTTP клиентов, где есть ссылка на некий commandLineExecutionContext - это служебный объект Tramvai который напрямую ссылается на Dependency Injection контейнер запроса.

Таким образом полный пример кода:

js 
const httpClientFactory = ({ ..., commandLineExecutionContext }) => {
const options = {
...,
createCache: createCache ? (cacheOptions) => createCache('memory', cacheOptions) : undefined,
}
}


Где Closure функции createCache теперь всегда ссылается на commandLineExecutionContext, который в свою очередь тянет ссылку на весь DI контейнер (`ChildContainer` на предыдущем скрине)

SuperOleg dev notes

27 Aug, 19:37


Привет!

За недавнее время появилось несколько статей про утечки памяти в JavaScript Closures:
- https://jakearchibald.com/2024/garbage-collection-and-closures/
- https://www.nico.fyi/blog/memory-issue-in-javascript-and-closures

Не знаю, совпадение или я просто начал обращать на это внимание, но приходится много профилировать приложения на утечки и эти замыкания меня буквально преследуют, по большей части ссылки на объекты очищаются, но наконец-то настоящая утечка через замыкание нашлась.

Сначала пару моментов про наши приложения.

На tbank.ru активно используются микрофронтенды и SSR.

Серверный рендеринг с микрофронтами устроен так:
- для каждого микрофронта есть точка входа для Node.js окружения (условно header.server.js)
- сервер скачивает эти скрипты, получает строки с JS кодом
- выполняет строки как JS код в изолированном окружении через vm модуль
- на выходе получает обычные React компоненты

Приложения на tbank.ru построены на нашем фреймворке Tramvai, построенном поверх механизма Dependency Injection.

Особенность этого механизма, что на сервере, на каждый запрос пользователя, создается Dependency Injection контейнер. В этом контейнере хранится все, от объекта запроса до итоговой HTML строки которую мы отдадим в ответ пользователю.

Из-за этой особенности как правило факт утечки найти легко - каждый контейнер может весить несколько мегабайт, при утечке эти контейнеры не будут очищаться через Garbage Collector при завершении запросов.

В данном случае интересен именно механизм утечки и цепочка ссылок до контейнера, который не убирал GC.

На скриншоте профайлера, на самом деле сразу видно всю цепочку, но из-за ее особенностей раскопал причину не сразу.

SuperOleg dev notes

01 Aug, 21:51


Привет!

Мысли вслух про экосистему вокруг мета-фреймворков.

Мы привыкли использовать многие инструменты как CLI, например сборщики, но наличие JS API у таких инструментов открывает большие возможности.

Давно было интересно как работает Nitro, и как работает фреймворк Vinxi у которого под капотом Nitro + Vite, и у кого какая область ответственности.

И в целом интересно как так быстро и легко мета-фреймворки новые появляются.

С Vinxi оказывается верхнеуровнего все просто:
- Nitro - билдер и дев сервер для http сервера
- Vite - билдер и дев сервер для фронта

Под капотом у обоих rollup для непосредственно сборки.

Vinxi просто запускает одновременно либо оба dev сервера либо обе production сборки.

Тут сразу хочется опыт Remix вспомнить, который теперь "всего лишь плагин для Vite".

Это конечно не значит, что можно взять условный Vite, и все работает из коробки, код для интеграции писать нужно.

Но это означает, что под современные потребности существуют достаточно высокоуровневые инструменты, с гибким API и хорошей экосистемой, которые позволяют создавать полноценные фреймворки поверх них.

А например SSR, React Server Components, всевозможные file-system роутинги и мгновенные hot reload'ы это актуальные потребности на сегодняшний день.

По моему опыту, инструментам трудно уходить от узкой специализации до широкого набора фичей, и оставаться быстрыми и пользователей оставлять довольными.

Судя по всему у Vite это получается и какой-то баланс найден.

Также, не ясно насколько хороший результат получается, когда фреймворки (Remix, Vinxi, SolidStart, Vike и так далее) собраны из таких инструментов, вместо написания специализированного кода под свои кейсы.

Как минимум это большой буст к скорости разработки, особенно для небольших команд.

С другой стороны есть опыт Vercel, которые делают инструменты непосредственно под фреймворк - Turbopack и Next.js, да и прямо скажем существующая интеграция webpack там очень не простая и многослойная.

В перспективе у некста все должно быть круто, но на текущий момент много репортов на проблемы со скоростью сборки.

Но тут и команда разработчиков мощная, сложно не верить в ребят.

Очень интересно как дальше будет развиваться экосистема, и очень хочется самому на коленке собрать Tramvai на основе Vite/Nitro/Vinxi, и посмотреть так ли все с ними хорошо.

SuperOleg dev notes

15 Jun, 22:40


Также, очень приятно видеть в твиттере много довольных мейнтейнеров open source проектов - Microsoft поддержал финансово внушительный список проектов - https://x.com/jeffwilcox/status/1801794149815095495?s=19

Вдохновляет ❤️

SuperOleg dev notes

15 Jun, 22:30


Не так давно писал про интересное изменение в React 19 - последовательная загрузка компонентов в рамках Suspense границ - https://t.me/super_oleg_dev/181

Как я понимаю это сильно ударило по перфу SPA приложений, где нет возможности предзагрузить ассеты и данные параллельно как это делают при SSR.

В итоге будут искать более удачное / универсальное решение:
- https://github.com/facebook/react/issues/29898
- https://x.com/sophiebits/status/1801663976973209620?s=19

SuperOleg dev notes

13 Jun, 11:09


TIL - поисковые боты могут парсить инлайн JS и JSON на странице и индексировать ссылки оттуда.

Почему это важно - для SSR приложений базовый механизм передать готовые данные (initial state) с сервера на клиент это как раз JSON в разметке, например как <script type="application/json">

Примеры проблемы:
- https://github.com/vercel/next.js/discussions/39377
- https://stackoverflow.com/questions/47210596/how-to-prevent-google-from-indexing-script-type-application-json-content

При этом адекватного решения проблемы не вижу.

Добавлять лишние кодирование - штраф к перформансу, даже пара ms повлияет на серверный рендеринг.
Собственно и так уже влияет - для JSON с initial state обязательно надо делать и перевод объекта в строку и экранирование, а на клиенте парсить обратно - это все не бесплатно (обычно вторая по нагрузке на CPU работа на сервере после renderToString, хотя и гораздо менее заметная)

Заодно скину ссылку как делаем экранирование стейта, там сразу парочка референсов и ссылка на возможные уязвимости - https://github.com/tramvaijs/tramvai/blob/main/packages/libs/safe-strings/src/encodeForJSContext.ts

Почему например не вынести в отдельный файл стейт - хорошо объясняется тут - https://github.com/vercel/next.js/discussions/42170#discussioncomment-8880248 (спасибо за ссылку @igor_katsuba)

SuperOleg dev notes

27 May, 12:32


Про "все-в-React" или "все-в-компоненте" и декларативность.

Что мы видели интересного в коде компонентов:
- <Route> и <Redirect> из React Router
- <Script> из Next.js и <Scripts> из Remix
- useQuery из React Query или Apollo
- useForm из React Hook Form
- не могу не вспомнить <FormSpy> из Formik

Компоненты и хуки из списка объединяет то, что это либо обернутые в компонент сайд-эффекты, либо размазанная по компонентам логика, которой не возможно управлять централизованно.

Это не примеры абсолютного зла - так как плюсы без сомнения есть, в очень гибком роутинге, либо в умном механизме запросов без бойлерплейта, либо в минималистичной абстракции логики фреймворка от пользователя.

Но в масштабе, проблемы есть.

Хранить список роутов в компонентах - не масштабируется, подход с заранее объявленными роутами гораздо легче развивать и поддерживать.

Но декларативность в React мире доходит до того, что например в React Router вообще нет такой цельной сущности как Router! Есть только набор хуков, а сделать что-то с роутингом вне компонентов мы просто не имеем возможности.

Например (императивно) создать const router = new Router(), предзаполнить router.addRoute(...), в лоадере/экшены выполнить router.redirect(..), передать в <Router.Provider router={router} > и так далее.

Отсутствует жизненный цикл, настолько RR связан с реактом. Механизм loader'ов добавил хотя бы один этап жизненного цикла. Но для какой-нибудь авторизации разработчики все-равно будут создавать всякие <Auth> и <ProtectedRoute> компоненты.

Наглядный пример в этой статье - https://blog.logrocket.com/authentication-react-router-v6/. Хороший гайд, все аккуратно, react way, но насколько же сильно размазана аутентификация по компонентам, и не существует снаружи.

Просто сравните с Angular, где через DI предоставляется отдельный сервис для аутентификации, используемый и в UI и в гуарде роутера.

React Query большие молодцы, выделяют логику в отдельные сущности, и с ними можно работать где угодно, например QueryClient. Такие вещи очень упрощают интеграцию для SSR фреймворка.

Но и тут есть проблемы, возьмем сами квери. Как отдельной сущности их просто нет, есть набор параметров вида const query = useQuery({ queryKey: ['todos'], queryFn: getTodos }), и только через queryKey возможна связь для одной и той же квери между использованием в компоненте и прямой работой через QueryClient в других местах.

Сложно делать расширяемые и переиспользуемые query, параметры считываются и сохраняются сразу при рендере хука - нельзя сделать queryKey функцией, на момент рендера useQuery надо иметь все параметры для формирования массива ключей.

Используя React Query и React Hook Form, можно делать хороший UX и писать мало кода на сложные кейсы, но очень сложно явно выделить сущности / модели / бизнес-логику, что опять-таки может выстрелить в ногу в масштабе, и уж точно не поможет сделать архитектуру "кричащей".

По поводу таких кейсов как Scripts в Remix, или поддержка метаданных и стилей в React 19.

Реакт или Ремикс не дает нам условный AssetsManager, в который мы смогли бы добавить ресурс явно по конкретному условию, например:
if (analyticsEnabled) {
assetManager.addScript({ src: anaylicsScript, async })
}


Нам предлагается явно рендерить нужный скрипт в компоненте (прямо или косвенно). И хотя для RSC и стриминга понятно почему важно поддержать эти механизмы, выбранный путь мне совершенно не нравится.

Если подвести итог, эта псевдо-декларативность в React и экосистеме, как и явно обозначенная ограниченная область ответственности React (не фреймворк, а библиотека!), напрямую влияет на то как сообщество пишет приложения, на образ мышления новых разработчиков, и как мне кажется влияет негативно.

Получается для меня, главная проблема React это точно не лишние ререндеры, а архитектурные вопросы. И вряд ли экосистема в этом плане заметно поменяется, а область ответственности React только расширяется. Скорее всего в будущем при поддержке RSC и прочих современных возможностей, остро встанет вопрос что мета-фреймворк либо будет написан в react way стиле, либо останется в стороне.

SuperOleg dev notes

27 May, 10:42


По поводу мира вне наших компонентов.

Для SPA-приложений, самый простой кейс это запросы, критичные для отрисовки страницы. Для SSR тоже валидно если говорим про кастомную реализацию, мета-фреймворки все-таки решают кейс давая механизм для загрузки данных под конкретный роут.

Стандартный паттерн - делать запросы в useEffect. Но это не эффективно, не всегда логично, и не всегда хорошо для UX и перформанса.

Не логично, потому что запрос за данными не обязан быть привязан к жизненному циклу конкретного компонента.
Он даже не обязан быть привязан к конкретному роуту приложения, но по роутам проще всего группировать сайд-эффекты, которые мы хотим запустить параллельно и которые важны для отрисовки.

Не эффективно, так как слишком поздно. Зачем ждать полный цикл рендера и отрисовки в браузере до старта запроса?

На конкретном примере, очень многие приложения используют React Router. До относительно свежей версии 6.4, этот роутер вообще не давал никаких инструментов для загрузки данных. Столкнулся с этим два года назад для пет проекта, удивился, не долго думая добавил костыль с загрузкой через тот же useEffect но c возможностью привязать запрос к компоненту страницы.

Поэтому для React Router появление механизма loader'ов для SPA-приложений это уже большой шаг вперед - в том числе для новых разработчиков, они будут видеть что запрос можно запустить где-то вне useEffect, и это нормально.

Это касается не только запросов, в компонентах лучше по максимуму ничего не делать кроме непосредственно отображения и обработки пользовательских событий.

Как пример, приведу список отдельных модулей для фреймворка Tramvai.
Там все не идеально, такие модули как router / render / server связаны сильнее чем хотелось бы.
Но в любом случае по списку наглядно что все реализовано по отдельности, за счет модульной архитектуры:
- рендеринг и гидрация
- роутинг
- обработка серверных запросов и инициализация сервера
- работа с SEO
- логгер / метрики / куки / client-hints
- интеграция с React Query отдельным модулем

И работать со всем этим мы также можем по отдельности.

Например, используя React Query, сбросить или обновить кэш на любом этапе жизненного цикла запроса за страницей, начиная от механизма экшенов, или сделать префетч конкретной query до рендеринга страницы, а потом использовать ее в компоненте.

SuperOleg dev notes

27 May, 10:17


Разберем предметно почему считаю это проблемами.

Про синглтоны.

Допустим, у нас кастомный SSR (специально спустимся на уровень ниже), на сервере делаем запросы, используем полученные данные для рендеринга контента.

Кроме этого, часто нужно много других возможностей - работа с куками, заголовками и статусом ответа, мета-тегами, в общем все то что упрощают для нас мета-фреймворки.

Мой любимый пример, для работы с cookies на сервере, нужен соответствующий объект запроса. Для удобства и переиспользования логики, можно выделить отдельный сервис для работы с куками. Этому сервису потребуется объект Request.

Представим в виде псевдо-кода:
class Cookies {
constructor(request) {}
get() {}
set() {}
}

Так как запросов один сервер обрабатывает много, мы не можем сделать синглтон вида export const cookies = new Cookies(request) - request будет доступен только в обработчике запроса, экземпляр сервиса надо создавать каждый раз новый.

Отдельно обсудим попозже как Next.js это все-таки реализует.

Передать созданный на запрос сервис в React компоненты не проблема - у нас уже будет код который на каждый запрос рендерит в строку некий рутовый <App /> компонент, и инстанс cookies можно передать через контекст.

Но почему вообще мы должны работать с куками в компонентах? Для запросов и прочей логики приложения нам нужен механизм для сайд-эффектов - в разных фреймворках это loaders / actions / async components и так далее. Именно в таких лоадерах нам понадобится сервис.

Представим лоадер с использованием сервиса в виде псевдо-кода:
async function loader(params) {
return api.getSomething().then((response) => {
cookies.set(foo, response.someUniqueField)
return response
})
}


Какие тут варианты получить api или cookies кроме импорта синглтона? Только интеграция с нашим SSR, и например передавать их в аргументы лоадера - const { api, cookies } = params в данном случае (например так позволяет сделать Remix).

Приложение растет, сервисов десятки, между ними много зависимостей - удобно ли это, гибко, масштабируется? Даст ли подходящий совет документация React?

Импорт cookies и headers в Next.js возможен только за счет использования Async Local Storage - и это очень подходящее использование технологии, но по аналогии с этими объектами, сможете ли вы как-то просто добавить свои сервисы для Next.js приложения?

Я уверен, что лучшее решение для кейса это механизм Dependency Injection, что уже давно широко используется на практике в JS экосистеме, например в Nest.js и Angular.

Но в React экосистеме это не популярная тема. Из последнего интересного, твит от одного из ведущих разработчиков фреймворка, что DI в React стоит делать через "exports" в package.json - https://x.com/sebmarkbage/status/1765828741500981475 - и хотя это интересная мысль, она закрывает только часть кейсов который может закрыть полноценный DI контейнер.

Также DI позволит радикально улучшить кодовую базу мета-фреймворков - сейчас в Next.js / Remix вы можете видеть огромные файлы с лапшой кода, которые реализуют сразу множество фичей. И такие же суровые Pull Request'ы, в которых эти файлы шатают.

Любое расширение этих фреймворков со стороны тоже как правило большая боль, потому что официальных точек расширения нет, помимо ручной конфигурации сборщика.

Даже без DI, пример фреймворка с плагинной архитектурой, у которых с расширением дела гораздо лучше - Nuxt.js.

Можно сравнить любой плагин, например PWA, для Next и для Nuxt:
- у Некста можно пошатать конфиг, обернуть рутовый компонент
- у Nuxt можно вклиниться на разные этапы жизненного цикла приложения, модифицировать и параметры билд тайма, и рантайма

SuperOleg dev notes

27 May, 10:17


Критика React или его экосистемы.

RSC концепция классная, но сложная. Сложная и в понимании, и в интеграции, особенно если говорить про миграцию большой кодовой базы.

В плане DX - есть как и сложности, так и плюсы. Мем про PHP код в React компонентах смешной, пока не понимаешь что есть люди кто действительно думают что серверные компоненты это возврат к каким-то древним временам - это не так, никто не требует писать SQL в компонентах, архитектура приложения и выбранные абстракции зависят только от вас.

RSC (в принципе это началось еще с Suspense и клиентских GraphQL библиотек) открывают все мощь паттерна render as you fetch - возможность минимальным количеством кода делать запросы рядом с компонентом, где эти данные будут использованы, и возможность писать код как бы без границ между сервером и клиентом.

Но появляются и проблемы:
- нужен мета-фреймворк который поможет избежать водопада и дублирования запросов (частично решается в 19 React)
- еще легче испортить архитектуру приложения, так как размазать логику запросов или все-таки написать напрямую этот SQL в компоненте стало проще

И тут мы плавно переходим к проблеме архитектуры React приложений в целом.

Как и любой другой фреймворк, реакт предлагает замкнуть все на себя:
- при SSR весь HTML начиная от <html> тега находится у нас в рутовом компоненте
- мира за пределами render(<App />) как будто не существует
- все должно быть декларативно и даже такие сайд-эффекты как редирект делаем компонентами
- все что хотим переиспользовать - либо синглтон либо через React контекст
- разделение отображения и бизнес-логики на плечах разработчиков

SuperOleg dev notes

25 May, 15:17


Еще более радикально улучшить ситуацию со временем ответа (да и производительностью серверов) поможет улучшить Partial Prerendering.

PPR тоже уже разбирали в канале, это экспериментальная фича именно реакта, хоть и используется и имеет документацию только в Next.js.

Позволяет получить на этапе сборки статичную часть приложения (App Shell), быстро отдать ее клиенту, красиво "вклеить" в нее динамическую часть в стриме ответа.

Ну и для радикального уменьшения клиентского кода, команда React предложила и реализовала широко обсуждаемые и осуждаемые React Server Components.

Доклад с анонсом RSC - https://react.dev/blog/2020/12/21/data-fetching-with-react-server-components

И RFC с особенностями и деталями реализации - https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md

Единственная production ready реализация RSC - у Next.js, и так как это полноценный фреймворк со своими интерфейсами и особенностями, иногда сложно понять какие плюсы и минусы RSC относятся именно к базовой реализации в React, а не в интеграции.

Да, RSC накладывают много ограничений. Да, полная смена архитектуры.

Но разве есть более радикальный способ уменьшить количество клиентского кода, чем оставить этот код на сервере?

Из значимых альтернатив я могу назвать только Qwik.js (
еще существуют Phoenix LiveView, Rails Hotwire - но это не знакомые мне экосистемы), который предоставляет ленивую загрузку кода вплоть до каждого обработчика событий. То есть кода будет меньше только на старте.

Кстати оба подхода сильно зависят от хорошего интернет соединения, надеюсь в будущем будет больше исследований по теме, становится ли лучше жизнь у реальных пользователей, или как всегда трейдофф.

SuperOleg dev notes

25 May, 14:33


Вернёмся к фичам.

React позволяет легко реализовать классический серверный рендеринг с гидрацией.

Любой SSR имеет несколько значимых проблем:
- долгое время ответа страницы (ждём запросы, рисуем HTML)
- долгое время до интерактивности (уже видим контент но не кликабельно до загрузки всего JS)
- толстый бандл (тащим код всех компонентов)

При этом SSR актуален, имеет много плюсов, и фреймворки по разному стараются решить эти проблемы:
- потоковый рендеринг начиная с Marko.js
- await/defer в Remix
- islands architecture в Astro
- resumability в Qwik.js

Тут можно упомянуть костыль с lazy hydration для React.

И React также предлагает решения для всех этих кейсов, и решения очень интересные, эффективные и инновационные, не более спорные чем все остальные.

Что-то приходится долго ждать, что-то требует кардинально менять архитектуру - все не идеально, но отмечу что новые фичи опциональны.

Когда открыли репозиторий с дискуссиями React Working Group про 18 версию, я половину ночи провел за чтением, настолько было интересно, так как там обсуждались проблемы с которыми я сталкиваюсь на практике, и технологии которые помогут их решить.

И опять таки опираясь на фундамент архитектуры с vDOM и Fiber, фреймворк предлагает нам следующие решения:

Полноценный потоковый рендеринг - с поддержкой Suspense на сервере, теперь можно быстро отдать App Shell, дождаться асинхронных действий на сервере, и досылать разметку по мере необходимости - что позволит улучшить и метрику TTFB и LCP.

И на практике, почему это круто - во-первых это фундамент для React Server Components, во-вторых на этом основан уже упомянутый await/defer - что я считаю самой крутой фичей для классического SSR, который не готов полностью переходить на стриминг с RSC.

Selective Hydration - больше не нужно загружать весь бандл для начала гидрации, фреймворк позволяет гидрировать отдельные части приложения по требованию (базовый кейс, по факту загрузки lazy компонентов).

Это позволяет сильно улучшить время до интерактивности приложения, причем радикально в комбинации со стримингом, так как на клиенте надо загрузить только код фреймворка и точку входа в приложение для начала гидрации.

Подробнее про новую SSR архитектуру в этой дискуссии и моих предыдущих постах.

Про SEO и стриминг интересно тут - https://github.com/vercel/next.js/discussions/50829

SuperOleg dev notes

25 May, 14:06


Про стабильность.

React одна из эталонных библиотек, с минимальным количеством ломающих изменений в публичном API.

Deprecated функционал проходит несколько мажорных версий, предупреждает команда реакта об этом и в документации, и в рантайме.

На сложные кейсы зачастую предоставляется готовый codemod.

Мне кажется что даже код десятилетней давности можно запустить на 18 реакте.

Для мета-фреймворка, без проблем получается обновлять мажорки, и поддерживать несколько мажорных версий одновременно.

Но быть early adopter не легко, на экспериментальные вещи часто не хватает документации (возможно не проблема если работаешь в Vercel) - поэтому никогда не спешим с интеграцией экспериментальных фич.

Про концепции.

Тут и про React, и про Redux, просто хочется отметить вещи, которые когда-то были для меня в новинку, заставили мыслить шире, и писать код лучше.

В первую очередь это функциональный подход. Хотя я до сих пор отношусь к ФП с интересом но одновременно со скептицизмом (всегда больше интересовали практические кейсы чем красивые концепции), такие базовые вещи как чистые функции, композиция, декларативность - положительно влияют на качество кода.

Сюда же идёт иммутабельность - насколько же до этого я писал сложный для отладки код где мутировал вложенные и вложенные объекты как попало...

Даже в реактивных стейт-менеджерах иммутабельность активно используется - это почти бесплатный дифф, и прозрачное обновление данных.

Нормализация - крутой подход к структурированию данных. Не всегда и везде нужен, но к примеру на архитектурных собесах мне часто попадаются кандидаты, кто выбирает Redux, а потом не может спроектировать оптимальную структуру стейта под задачу, и нормализация решила бы все их проблемы (может уже не в тренде?).

SuperOleg dev notes

24 May, 17:56


Отвлечемся поговорить про перформанс.

Наверное каждый эксперт в отладке и ускорении React приложений прошел непростой путь оптимизации на проде многоступенчатых форм или сложных таблиц, и знает все способы как избежать лишних ререндеров (которые не проблема сами по себе, но проблема в масштабе).

И каждый эксперт скорее всего не хотел бы этого знать и об этом думать.

Я согласен, что механизм обновления в React по умолчанию заставляет нас делать больше работы и предварительных оптимизаций (а дефолты это очень важно), либо не делать и потом все-таки становиться экспертами по реакт перформансу.

Это не означает, что в других фреймворках ничего не нужно оптимизировать, проблемы зачастую в пользовательском коде. Но по умолчанию - да, тормоза в React приложении поймать легче. При этом, мы практически всегда знаем что с этим делать.

Можно рассматривать эту проблему как сравнение Virtual DOM против реактивности и сигналов.

Пару ссылок про проблемы Virtual DOM:
- https://svelte.dev/blog/virtual-dom-is-pure-overhead
- https://vuejs.org/guide/extras/rendering-mechanism#compiler-informed-virtual-dom

Но не будем забывать про плюсы, то что уже обсуждали в предыдущем посте. Благодаря Virtual DOM и Fiber архитектуре, в реакте реализовыван прерываемый рендеринг, который открывает пачку крутых UX паттернов (suspense, транзишены и так далее).

Отличный обзор concurrent фич от Ивана Акулова - https://3perf.com/talks/react-concurrency/

Если говорить про другие фреймворки, интересные мысли можно почитать по ключевым словам типа Suspense на гитхабе в соответствующих проектах, например:
- https://github.com/sveltejs/svelte/issues/1736
- https://github.com/sveltejs/svelte/issues/3203#issuecomment-797346259

И мысли разные, в том числе от авторов фреймворков - что-то реализовать можно, что-то сложно, что-то не нужно.
Как и везде, серебряной пули нет, а плохие и медленные приложения писать с использованием сигналов также легко.

Также одна из важных вещей в Реакт - консистентность состояния.

Итого:
- по умолчанию в React легко написать медленный код
- это решаемая проблема, но бойлерплейт/надо думать
- текущая архитектура с Virtual DOM имеет и преимущества над другими решениями (компилируемыми / реактивными)

Добавлю еще коротко про React Forget.

Концептуально все просто - поможет оставить исходный код чистым, а продакшн код производительным.

Считаю это крутым экспериментальным проектом, надеюсь на его успех.

И это точно не менее предсказуемый инструмент чем любой compile-time фреймворк (такие мнения тоже встречал).

SuperOleg dev notes

24 May, 17:55


Почему я люблю React и считаю его развитие последовательным, гармоничным и в целом работой крутых инженеров, которые думают о своих пользователях. Что именно сделало меня лучше как инженера. И конечно что я считаю минусами фреймворка либо экосистемы.

Начну наверное с Fiber архитектуры - а именно переход React на Fiber является фундаментом, благодаря которому возможны последующие фичи, такие как хуки и Suspense, Concurrent Rendering и Selective Hydration, React Server Components и Partial Prerendering.

Fiber открыл ряд возможностей, например:
- рендерить компоненты по отдельности и с разным приоритетом
- ставить работу на паузу и возобновлять ее

Это действительно крутое стратегическое решения для архитектуры React (много ли вещей вы продумали в своей работе на 8+ лет вперед?), и что немаловажно, нацеленное на лучший UX, так как длинные блокирующие задачи это одна из самых значимых проблем производительности веб-приложений.

Лучше про перф за меня расскажет Ден в этом докладе - https://www.youtube.com/watch?v=nLF0n9SACd4&ab_channel=JSConf.

Я встречал и критикау демок от React тимы с Suspense и конкурентным рендерингом и приоритезацией апдейтов на пользовательский ввод - что либо это не real world кейсы, либо перфа можно достичь и другими способами.
Не знаю, я тут вижу только крутые фичи у которых простая концептуальная база - разделение больших задач на более мелкие.
Можем обсудить в комментариях)

Следующий майндшифт - это хуки. Раньше у нас были для работы с состоянием и контекстом классовые компоненты (миксины я практически не застал), с относительно сложным жизненным циклом и действительно сложными механизмами для синхронизации стейта и пропсов, и отсутствием адекватного механизма для переиспользования логики (всей душой не люблю паттерн с композицией пачки HOC'ов).

Появление хуков - это естественная эволюция реакта, они можно сказать "напрашивались". На самом деле, конечно я не знал как будут выглядеть хуки до их анонса. Но именно концепция хуков очень органично вписывается в построение интерфейсов на функциональных React компонентах.

Что мы получили:
- простой цикл обновления компонента (useEffect подписка/очистка)
- и также предсказуемый (массив зависимостей)
- удобная работа со стейтом и контекстом
- нативный механизм для переиспользования логики

Но это возможно дело случая - для меня хуки сразу вписались в ментальную модель, которую я держу в голове разрабатывая на React, даже до начала их практического использования. У других разработчиков хуки вызвали отторжение. В общем как с любой другой новой технологией (из свежего это сигналы в Angular, руны в Svelte).

Также репутацию хуков подпортили попытки инкапсулировать в них бизнес-логику.
Мне кажется, кастомные хуки это про два основных кейса:
- переиспользовать UI логику, когда работаем с ref либо с DOM деревом напрямую
- переиспользовать утилитарную логику, общие кейсы работы с состоянием или жизненным циклом (например useSelector, useQuery, useToggle, useTimeout)