Про стабильность.
React одна из эталонных библиотек, с минимальным количеством ломающих изменений в публичном API.
Deprecated функционал проходит несколько мажорных версий, предупреждает команда реакта об этом и в документации, и в рантайме.
На сложные кейсы зачастую предоставляется готовый codemod.
Мне кажется что даже код десятилетней давности можно запустить на 18 реакте.
Для мета-фреймворка, без проблем получается обновлять мажорки, и поддерживать несколько мажорных версий одновременно.
Но быть early adopter не легко, на экспериментальные вещи часто не хватает документации (возможно не проблема если работаешь в Vercel) - поэтому никогда не спешим с интеграцией экспериментальных фич.
Про концепции.
Тут и про React, и про Redux, просто хочется отметить вещи, которые когда-то были для меня в новинку, заставили мыслить шире, и писать код лучше.
В первую очередь это функциональный подход. Хотя я до сих пор отношусь к ФП с интересом но одновременно со скептицизмом (всегда больше интересовали практические кейсы чем красивые концепции), такие базовые вещи как чистые функции, композиция, декларативность - положительно влияют на качество кода.
Сюда же идёт иммутабельность - насколько же до этого я писал сложный для отладки код где мутировал вложенные и вложенные объекты как попало...
Даже в реактивных стейт-менеджерах иммутабельность активно используется - это почти бесплатный дифф, и прозрачное обновление данных.
Нормализация - крутой подход к структурированию данных. Не всегда и везде нужен, но к примеру на архитектурных собесах мне часто попадаются кандидаты, кто выбирает Redux, а потом не может спроектировать оптимальную структуру стейта под задачу, и нормализация решила бы все их проблемы (может уже не в тренде?).
React одна из эталонных библиотек, с минимальным количеством ломающих изменений в публичном API.
Deprecated функционал проходит несколько мажорных версий, предупреждает команда реакта об этом и в документации, и в рантайме.
На сложные кейсы зачастую предоставляется готовый codemod.
Мне кажется что даже код десятилетней давности можно запустить на 18 реакте.
Для мета-фреймворка, без проблем получается обновлять мажорки, и поддерживать несколько мажорных версий одновременно.
Но быть early adopter не легко, на экспериментальные вещи часто не хватает документации (возможно не проблема если работаешь в Vercel) - поэтому никогда не спешим с интеграцией экспериментальных фич.
Про концепции.
Тут и про React, и про Redux, просто хочется отметить вещи, которые когда-то были для меня в новинку, заставили мыслить шире, и писать код лучше.
В первую очередь это функциональный подход. Хотя я до сих пор отношусь к ФП с интересом но одновременно со скептицизмом (всегда больше интересовали практические кейсы чем красивые концепции), такие базовые вещи как чистые функции, композиция, декларативность - положительно влияют на качество кода.
Сюда же идёт иммутабельность - насколько же до этого я писал сложный для отладки код где мутировал вложенные и вложенные объекты как попало...
Даже в реактивных стейт-менеджерах иммутабельность активно используется - это почти бесплатный дифф, и прозрачное обновление данных.
Нормализация - крутой подход к структурированию данных. Не всегда и везде нужен, но к примеру на архитектурных собесах мне часто попадаются кандидаты, кто выбирает Redux, а потом не может спроектировать оптимальную структуру стейта под задачу, и нормализация решила бы все их проблемы (может уже не в тренде?).
GitHub
Add react 19 codemods and recommend `codemod` command by DmytroHryshyn · Pull Request #320 · reactjs/react-codemod
adds 7 react 19 codemods
adds ability to run ts transfroms from react-codemod CLI
Testing
Running with react-codemod
npx react-codemod remove-context-provider <path>
npx react-codemod replac...
adds ability to run ts transfroms from react-codemod CLI
Testing
Running with react-codemod
npx react-codemod remove-context-provider <path>
npx react-codemod replac...
👍14❤5🔥1😁1
Вернёмся к фичам.
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, во-вторых на этом основан уже упомянутый
Selective Hydration - больше не нужно загружать весь бандл для начала гидрации, фреймворк позволяет гидрировать отдельные части приложения по требованию (базовый кейс, по факту загрузки lazy компонентов).
Это позволяет сильно улучшить время до интерактивности приложения, причем радикально в комбинации со стримингом, так как на клиенте надо загрузить только код фреймворка и точку входа в приложение для начала гидрации.
Подробнее про новую SSR архитектуру в этой дискуссии и моих предыдущих постах.
Про SEO и стриминг интересно тут - https://github.com/vercel/next.js/discussions/50829
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
www.patterns.dev
Streaming Server-Side Rendering
Generate HTML to be rendered on the server in response to a user request
👍8❤🔥2🔥1
Еще более радикально улучшить ситуацию со временем ответа (да и производительностью серверов) поможет улучшить 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 - но это не знакомые мне экосистемы), который предоставляет ленивую загрузку кода вплоть до каждого обработчика событий. То есть кода будет меньше только на старте.
Кстати оба подхода сильно зависят от хорошего интернет соединения, надеюсь в будущем будет больше исследований по теме, становится ли лучше жизнь у реальных пользователей, или как всегда трейдофф.
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 - но это не знакомые мне экосистемы), который предоставляет ленивую загрузку кода вплоть до каждого обработчика событий. То есть кода будет меньше только на старте.
Кстати оба подхода сильно зависят от хорошего интернет соединения, надеюсь в будущем будет больше исследований по теме, становится ли лучше жизнь у реальных пользователей, или как всегда трейдофф.
nextjs.org
App Router: Partial Prerendering
An early look into Partial Prerendering and how it works.
👍10🔥3
Критика React или его экосистемы.
RSC концепция классная, но сложная. Сложная и в понимании, и в интеграции, особенно если говорить про миграцию большой кодовой базы.
В плане DX - есть как и сложности, так и плюсы. Мем про PHP код в React компонентах смешной, пока не понимаешь что есть люди кто действительно думают что серверные компоненты это возврат к каким-то древним временам - это не так, никто не требует писать SQL в компонентах, архитектура приложения и выбранные абстракции зависят только от вас.
RSC (в принципе это началось еще с Suspense и клиентских GraphQL библиотек) открывают все мощь паттерна render as you fetch - возможность минимальным количеством кода делать запросы рядом с компонентом, где эти данные будут использованы, и возможность писать код как бы без границ между сервером и клиентом.
Но появляются и проблемы:
- нужен мета-фреймворк который поможет избежать водопада и дублирования запросов (частично решается в 19 React)
- еще легче испортить архитектуру приложения, так как размазать логику запросов или все-таки написать напрямую этот SQL в компоненте стало проще
И тут мы плавно переходим к проблеме архитектуры React приложений в целом.
Как и любой другой фреймворк, реакт предлагает замкнуть все на себя:
- при SSR весь HTML начиная от
- мира за пределами
- все должно быть декларативно и даже такие сайд-эффекты как редирект делаем компонентами
- все что хотим переиспользовать - либо синглтон либо через React контекст
- разделение отображения и бизнес-логики на плечах разработчиков
RSC концепция классная, но сложная. Сложная и в понимании, и в интеграции, особенно если говорить про миграцию большой кодовой базы.
В плане DX - есть как и сложности, так и плюсы. Мем про PHP код в React компонентах смешной, пока не понимаешь что есть люди кто действительно думают что серверные компоненты это возврат к каким-то древним временам - это не так, никто не требует писать SQL в компонентах, архитектура приложения и выбранные абстракции зависят только от вас.
RSC (в принципе это началось еще с Suspense и клиентских GraphQL библиотек) открывают все мощь паттерна render as you fetch - возможность минимальным количеством кода делать запросы рядом с компонентом, где эти данные будут использованы, и возможность писать код как бы без границ между сервером и клиентом.
Но появляются и проблемы:
- нужен мета-фреймворк который поможет избежать водопада и дублирования запросов (частично решается в 19 React)
- еще легче испортить архитектуру приложения, так как размазать логику запросов или все-таки написать напрямую этот SQL в компоненте стало проще
И тут мы плавно переходим к проблеме архитектуры React приложений в целом.
Как и любой другой фреймворк, реакт предлагает замкнуть все на себя:
- при SSR весь HTML начиная от
<html>
тега находится у нас в рутовом компоненте- мира за пределами
render(<App />)
как будто не существует- все должно быть декларативно и даже такие сайд-эффекты как редирект делаем компонентами
- все что хотим переиспользовать - либо синглтон либо через React контекст
- разделение отображения и бизнес-логики на плечах разработчиков
Epic React
Render as you fetch (with and without suspense)
Speed up your app's loading of code/data/assets with "render as you fetch" with and without React Suspense for Data Fetching
👍15💩4
Разберем предметно почему считаю это проблемами.
Про синглтоны.
Допустим, у нас кастомный SSR (специально спустимся на уровень ниже), на сервере делаем запросы, используем полученные данные для рендеринга контента.
Кроме этого, часто нужно много других возможностей - работа с куками, заголовками и статусом ответа, мета-тегами, в общем все то что упрощают для нас мета-фреймворки.
Мой любимый пример, для работы с cookies на сервере, нужен соответствующий объект запроса. Для удобства и переиспользования логики, можно выделить отдельный сервис для работы с куками. Этому сервису потребуется объект Request.
Представим в виде псевдо-кода:
Так как запросов один сервер обрабатывает много, мы не можем сделать синглтон вида
Отдельно обсудим попозже как Next.js это все-таки реализует.
Передать созданный на запрос сервис в React компоненты не проблема - у нас уже будет код который на каждый запрос рендерит в строку некий рутовый
Но почему вообще мы должны работать с куками в компонентах? Для запросов и прочей логики приложения нам нужен механизм для сайд-эффектов - в разных фреймворках это loaders / actions / async components и так далее. Именно в таких лоадерах нам понадобится сервис.
Представим лоадер с использованием сервиса в виде псевдо-кода:
Какие тут варианты получить
Приложение растет, сервисов десятки, между ними много зависимостей - удобно ли это, гибко, масштабируется? Даст ли подходящий совет документация React?
Импорт
Я уверен, что лучшее решение для кейса это механизм 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 можно вклиниться на разные этапы жизненного цикла приложения, модифицировать и параметры билд тайма, и рантайма
Про синглтоны.
Допустим, у нас кастомный 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 можно вклиниться на разные этапы жизненного цикла приложения, модифицировать и параметры билд тайма, и рантайма
X (formerly Twitter)
Sebastian Markbåge (@sebmarkbage) on X
This is also how most dependency injection in React apps should be done. Not Context.
Import a named default value from JS. Override it in package.json imports.
https://t.co/taaFQW5vG3
Import a named default value from JS. Override it in package.json imports.
https://t.co/taaFQW5vG3
👍14❤1
По поводу мира вне наших компонентов.
Для SPA-приложений, самый простой кейс это запросы, критичные для отрисовки страницы. Для SSR тоже валидно если говорим про кастомную реализацию, мета-фреймворки все-таки решают кейс давая механизм для загрузки данных под конкретный роут.
Стандартный паттерн - делать запросы в
Не логично, потому что запрос за данными не обязан быть привязан к жизненному циклу конкретного компонента.
Он даже не обязан быть привязан к конкретному роуту приложения, но по роутам проще всего группировать сайд-эффекты, которые мы хотим запустить параллельно и которые важны для отрисовки.
Не эффективно, так как слишком поздно. Зачем ждать полный цикл рендера и отрисовки в браузере до старта запроса?
На конкретном примере, очень многие приложения используют React Router. До относительно свежей версии 6.4, этот роутер вообще не давал никаких инструментов для загрузки данных. Столкнулся с этим два года назад для пет проекта, удивился, не долго думая добавил костыль с загрузкой через тот же useEffect но c возможностью привязать запрос к компоненту страницы.
Поэтому для React Router появление механизма loader'ов для SPA-приложений это уже большой шаг вперед - в том числе для новых разработчиков, они будут видеть что запрос можно запустить где-то вне useEffect, и это нормально.
Это касается не только запросов, в компонентах лучше по максимуму ничего не делать кроме непосредственно отображения и обработки пользовательских событий.
Как пример, приведу список отдельных модулей для фреймворка Tramvai.
Там все не идеально, такие модули как router / render / server связаны сильнее чем хотелось бы.
Но в любом случае по списку наглядно что все реализовано по отдельности, за счет модульной архитектуры:
- рендеринг и гидрация
- роутинг
- обработка серверных запросов и инициализация сервера
- работа с SEO
- логгер / метрики / куки / client-hints
- интеграция с React Query отдельным модулем
И работать со всем этим мы также можем по отдельности.
Например, используя React Query, сбросить или обновить кэш на любом этапе жизненного цикла запроса за страницей, начиная от механизма экшенов, или сделать префетч конкретной query до рендеринга страницы, а потом использовать ее в компоненте.
Для 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 до рендеринга страницы, а потом использовать ее в компоненте.
GitHub
feat: add route loader component · SuperOleg39/mind-basket@8cd3a38
Contribute to SuperOleg39/mind-basket development by creating an account on GitHub.
👍9
Про "все-в-React" или "все-в-компоненте" и декларативность.
Что мы видели интересного в коде компонентов:
-
-
-
-
- не могу не вспомнить
Компоненты и хуки из списка объединяет то, что это либо обернутые в компонент сайд-эффекты, либо размазанная по компонентам логика, которой не возможно управлять централизованно.
Это не примеры абсолютного зла - так как плюсы без сомнения есть, в очень гибком роутинге, либо в умном механизме запросов без бойлерплейта, либо в минималистичной абстракции логики фреймворка от пользователя.
Но в масштабе, проблемы есть.
Хранить список роутов в компонентах - не масштабируется, подход с заранее объявленными роутами гораздо легче развивать и поддерживать.
Но декларативность в React мире доходит до того, что например в React Router вообще нет такой цельной сущности как Router! Есть только набор хуков, а сделать что-то с роутингом вне компонентов мы просто не имеем возможности.
Например (императивно) создать
Отсутствует жизненный цикл, настолько RR связан с реактом. Механизм loader'ов добавил хотя бы один этап жизненного цикла. Но для какой-нибудь авторизации разработчики все-равно будут создавать всякие
Наглядный пример в этой статье - 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, параметры считываются и сохраняются сразу при рендере хука - нельзя сделать
Используя React Query и React Hook Form, можно делать хороший UX и писать мало кода на сложные кейсы, но очень сложно явно выделить сущности / модели / бизнес-логику, что опять-таки может выстрелить в ногу в масштабе, и уж точно не поможет сделать архитектуру "кричащей".
По поводу таких кейсов как
Реакт или Ремикс не дает нам условный
Нам предлагается явно рендерить нужный скрипт в компоненте (прямо или косвенно). И хотя для RSC и стриминга понятно почему важно поддержать эти механизмы, выбранный путь мне совершенно не нравится.
Если подвести итог, эта псевдо-декларативность в React и экосистеме, как и явно обозначенная ограниченная область ответственности React (не фреймворк, а библиотека!), напрямую влияет на то как сообщество пишет приложения, на образ мышления новых разработчиков, и как мне кажется влияет негативно.
Получается для меня, главная проблема React это точно не лишние ререндеры, а архитектурные вопросы. И вряд ли экосистема в этом плане заметно поменяется, а область ответственности React только расширяется. Скорее всего в будущем при поддержке RSC и прочих современных возможностей, остро встанет вопрос что мета-фреймворк либо будет написан в react way стиле, либо останется в стороне.
Что мы видели интересного в коде компонентов:
-
<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 стиле, либо останется в стороне.
GitHub
Add useRouter Hook · Issue #6430 · remix-run/react-router
Hi guys, at current React Conf Hooks have been announced for React v16.7. What's the plan for supporting them? Will there be a useRouter() any time soon? https://reactjs.org/docs/hooks-faq.html...
🔥22❤1👍1
TIL - поисковые боты могут парсить инлайн JS и JSON на странице и индексировать ссылки оттуда.
Почему это важно - для SSR приложений базовый механизм передать готовые данные (initial state) с сервера на клиент это как раз 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)
Почему это важно - для 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)
GitHub
google crawling link in __NEXT_DATA__ · vercel/next.js · Discussion #39377
Google crawling and SSR rendering Might be i'm wrong but we are getting url hits which are actually exist in __NEXT_DATA__ but not in HTML content. ex. few data of __NEXT_DATA__ "coverMedi...
👍9❤3🔥1🤔1
Не так давно писал про интересное изменение в 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
Как я понимаю это сильно ударило по перфу SPA приложений, где нет возможности предзагрузить ассеты и данные параллельно как это делают при SSR.
В итоге будут искать более удачное / универсальное решение:
- https://github.com/facebook/react/issues/29898
- https://x.com/sophiebits/status/1801663976973209620?s=19
Telegram
SuperOleg dev notes
Парочка интересных кейсы из беты React 19, которые особо не освещались:
- Прощай useIsomorphicLayoutEffect, больше нет варнинга на сервере - https://github.com/facebook/react/pull/26395
- Если вы используете throw promise или либы с поддержкой Suspense…
- Прощай useIsomorphicLayoutEffect, больше нет варнинга на сервере - https://github.com/facebook/react/pull/26395
- Если вы используете throw promise или либы с поддержкой Suspense…
👍3
Также, очень приятно видеть в твиттере много довольных мейнтейнеров open source проектов - Microsoft поддержал финансово внушительный список проектов - https://x.com/jeffwilcox/status/1801794149815095495?s=19
Вдохновляет ❤️
Вдохновляет ❤️
❤6
Привет!
Мысли вслух про экосистему вокруг мета-фреймворков.
Мы привыкли использовать многие инструменты как 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, и посмотреть так ли все с ними хорошо.
Мысли вслух про экосистему вокруг мета-фреймворков.
Мы привыкли использовать многие инструменты как 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, и посмотреть так ли все с ними хорошо.
GitHub
vinxi/packages/vinxi/lib/dev-server.js at main · nksaraf/vinxi
The Full Stack JavaScript SDK. Contribute to nksaraf/vinxi development by creating an account on GitHub.
👍8🤔5
Привет!
За недавнее время появилось несколько статей про утечки памяти в 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.
На скриншоте профайлера, на самом деле сразу видно всю цепочку, но из-за ее особенностей раскопал причину не сразу.
За недавнее время появилось несколько статей про утечки памяти в 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.
На скриншоте профайлера, на самом деле сразу видно всю цепочку, но из-за ее особенностей раскопал причину не сразу.
🔥3
Раскручиваем начиная с конца, и это у нас - HttpClient.
В коде HTTP клиента есть безобидная строчка - создается стрелочная функция, если упростить:
И тут наше первое замыкание, которое еще само по себе не проблема.
Эта функция создается в контексте фабрики HTTP клиентов, где есть ссылка на некий
Таким образом полный пример кода:
Где Closure функции
В коде 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` на предыдущем скрине)GitHub
tramvai/packages/modules/http-client/src/httpClient/httpClientFactory.ts at main · tramvaijs/tramvai
A modular framework for universal JS applications. Contribute to tramvaijs/tramvai development by creating an account on GitHub.
🔥7👍1
Идем дальше.
До утечки не было никаких криминальных изменений, в профайлере видно, что не очищается ссылка на трамвайный HTTP клиент, который теперь стал использоваться в модулях, которые занимаются всем вокруг загрузки и выполнения кода микрофронтов.
Но по цепочке видно, а также по приложенному стектрейсу, что утечка начинается изнутри кода микрофронтов, которые мы выполняем в изолированном контексте!
Начнем с функции - загрузчика
Максимально упрощенный код:
Дальше, уже странность. Есть отдельный метод, он используется внутри функции загрузчика, но в его замыкании тоже есть ссылка на httpClient:
Затем есть вообще анонимная функция, которая также в замыкании содержит ссылку на httpClient, и именно она выполняется внутри кода микрофронта.
До утечки не было никаких криминальных изменений, в профайлере видно, что не очищается ссылка на трамвайный HTTP клиент, который теперь стал использоваться в модулях, которые занимаются всем вокруг загрузки и выполнения кода микрофронтов.
Но по цепочке видно, а также по приложенному стектрейсу, что утечка начинается изнутри кода микрофронтов, которые мы выполняем в изолированном контексте!
Начнем с функции - загрузчика
getMM
- HttpClient передается туда явно, из приложения, присутствие объекта в замыкании ожидаемо.Максимально упрощенный код:
const getMM = ({ httpClient }) => {
...
}
Дальше, уже странность. Есть отдельный метод, он используется внутри функции загрузчика, но в его замыкании тоже есть ссылка на httpClient:
js
const stringToObject = (data) => {
// именно тут в closure вижу httpClient
...
}
const getMM = ({ httpClient }) => {
...
stringToObject(data)
...
}
Затем есть вообще анонимная функция, которая также в замыкании содержит ссылку на httpClient, и именно она выполняется внутри кода микрофронта.
👍6
Причина просто прекрасна - на этапе сборке, судя по всему этим занимается именно Terser, декларации функций перемещаются в место их использования.
На примере выше, код превращается примерно в такой:
Дальше разберем как эта ссылка утекает в код микрофронта.
Код в методе
Эту фабрику мы тут же вызываем с необходимыми аргументами, один из которых функция из этого же файла, условно:
Конечно же, объявление функции customRequire переместилось и превратилось в анонимную функцию по месту использования:
На скриншоте оригинальный собранный код, только после форматирования в профайлере.
На примере выше, код превращается примерно в такой:
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)
...
}
На скриншоте оригинальный собранный код, только после форматирования в профайлере.
🤯13
Итак, а что же утекает?
Дело в том, что для предотвращения лишних запросов за серверным кодом микрофронтов и экономии на парсинге строки в код, результаты работы этого загрузчика кэшируются в LRU-кэше.
И вот эта маленькая и безобидная стрелочная функция со всем своим богатым Closure сохраняется в памяти приложения практически на все время его жизни.
В приложениях могут быть сотни страниц, на которых используются десятки разных микрофронтов.
А в Tramvai есть модуль прогрева кэшей, который на старте сервера делает запросы к каждому роуту приложения.
Таким образом сразу после релиза, кэши прогреты, приложения работают быстрее, но и потребление памяти растет сразу при наличии утечки.
Интересно, что у нас есть тест на утечки памяти который недавно актуализировали - но такую комбинацию модулей и большого количества страниц и микрофронтов как у проблемного приложения мы вряд ли воспроизведем в этом тесте.
Также, не до конца понятно на каких уровнях надо чинить утечку.
С одной стороны не хочется трястись над каждым замыканием и бояться создавать новые функции (а как мы видим это и тулинг может сделать без нашего участия).
С другой стороны хочется предотвратить возможность выстрела в ногу, и исправить такие места как
В общем есть о чем подумать, интересны ваши мысли и опыт исправления таких вещей.
Дело в том, что для предотвращения лишних запросов за серверным кодом микрофронтов и экономии на парсинге строки в код, результаты работы этого загрузчика кэшируются в LRU-кэше.
И вот эта маленькая и безобидная стрелочная функция со всем своим богатым Closure сохраняется в памяти приложения практически на все время его жизни.
В приложениях могут быть сотни страниц, на которых используются десятки разных микрофронтов.
А в Tramvai есть модуль прогрева кэшей, который на старте сервера делает запросы к каждому роуту приложения.
Таким образом сразу после релиза, кэши прогреты, приложения работают быстрее, но и потребление памяти растет сразу при наличии утечки.
Интересно, что у нас есть тест на утечки памяти который недавно актуализировали - но такую комбинацию модулей и большого количества страниц и микрофронтов как у проблемного приложения мы вряд ли воспроизведем в этом тесте.
Также, не до конца понятно на каких уровнях надо чинить утечку.
С одной стороны не хочется трястись над каждым замыканием и бояться создавать новые функции (а как мы видим это и тулинг может сделать без нашего участия).
С другой стороны хочется предотвратить возможность выстрела в ногу, и исправить такие места как
createCache
с ссылкой на commandLineExecutionContext
.В общем есть о чем подумать, интересны ваши мысли и опыт исправления таких вещей.
🤔8
Возможно будет история еще об одной утечке, связанной с Async Local Storage, но как минимум хочу рассказать про интересный кейс, связанный с отладкой этой проблемы.
Мы используем Fastify в качестве веб-сервера для Tramvai, разбираясь с утечкой поставил логи на хуки onRequest и onResponse.
Даю нагрузку на приложение через autocannon, обнаружил что onResponse логов меньше чем onRequest, хотя ответы от приложения приходят все.
Хорошо (хоть и поздно) заметил что не хватает ровно столько onResponse сколько запросов к параллельному вызову я указал для autocannon, условные 10 последних из 100 отправленных.
Похоже, autocannon не дожидается последнюю пачку запросов до конца, и прерывает их до того как стрим ответа будет завершен.
Обнаружил это через подписку на события
Понаставил логов в fastify, посмотрел исходники, и оказывается хук
И это получается дефолтное поведение в 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 не самый надежный вариант для очистки чего-либо после завершения запроса.
Мы используем 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 не самый надежный вариант для очистки чего-либо после завершения запроса.
fastify.dev
Hooks | Fastify
👍13
Крутой внутренний продукт вышел на публику - https://t.me/unidrawio
Пользуюсь регулярно, в том числе проектировал в нем Microfrontends Platform (о котором уже рассказывал в этом канале), приятно порекламировать.
Пользуюсь регулярно, в том числе проектировал в нем Microfrontends Platform (о котором уже рассказывал в этом канале), приятно порекламировать.
Telegram
Unidraw.io
Канал про новости в комьюнити Unidraw - визуализируй вместе.
❤🔥7❤2👍2👎1
Привет!
Небольшой но интересный баг с микрофронтами, вебпаком и loadable.
Ранее я уже писал про интеграцию loadable для создания многостраничных микрофронтов Child Apps с разделением кода - https://t.me/super_oleg_dev/183
Вебпак собирает отдельные модуля в чанки, внутри они хранятся в мапе вида:
Где цифры - уникальные id этих модулей.
Модули из разных чанков после загрузки затем попадают в общую мапу, из которой вебпак будет доставать их при импорте.
Исследуя ошибку, увидел что в экспорте микрофронта получаю объект с переменными из нашего UI-kit.
Сначала грешил на Module Federation, так или иначе все стектрейсы проходят через него.
Но в итоге увидел, что вебпак ID для модуля микрофронта Foo в его чанке такой же, как ID модуля переменных UI-kit, внимание, в чанке другого микрофронта Bar.
Bar загружается раньше, его модуль попадает в общую мапу, и далее при импорте Foo, по указанному ID мы получаем эти переменные вместо микрофронта.
Проблема конечно же в глобальной мапе, в нашем случае это переменная:
В которую попадает тысячи модулей.
А учитывая независимые сборки микрофронтов, попасть на такую коллизию было просто делом времени.
Оказывается, вебпак плагин Loadable переопределяет эту переменную для сборки.
Хорошо что есть из коробки возможность переопределить эту переменную, сделал уникальной для каждого микрофронта:
Проверил разные кейсы, шаринг через Module Federation не пострадал, ошибка ушла.
Потом собрал без Loadable плагина, увидел что создается уникальное название, для моего чанка -
То есть проблема появилась только при интеграции нового функционала.
Пример такой проблемы, о которой даже знать и думать не будешь, пока не столкнешься.
И тот случай где ну очень удобно делать отладку через Chrome Overrides, без необходимости публиковать обновленный код пачки микрофронтов.
Небольшой но интересный баг с микрофронтами, вебпаком и 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, без необходимости публиковать обновленный код пачки микрофронтов.
Telegram
SuperOleg dev notes
Привет!
Свежий кейс отладки, вряд ли будет полезен (специфичный), но хочется его проговорить и забыть.
В Tramvai есть своя реализация микрофронтов с поддержкой Module Federation - Child Apps.
Какое-то время назад добавлял для Child Apps поддержку разделения…
Свежий кейс отладки, вряд ли будет полезен (специфичный), но хочется его проговорить и забыть.
В Tramvai есть своя реализация микрофронтов с поддержкой Module Federation - Child Apps.
Какое-то время назад добавлял для Child Apps поддержку разделения…
👍16🔥6❤1
Привет!
Попалась очень интересная статья от 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.
Попалась очень интересная статья от 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.
www.gitpod.io
We’re leaving Kubernetes - Blog
Discover why Gitpod is moving away from Kubernetes for cloud development environments after 6 years of experience at scale. Learn about Gitpod Flex and our new approach to development infrastructure for developers.
👍6❤3