SuperOleg dev notes
1.87K subscribers
56 photos
149 links
Обзоры новостей и статей из мира frontend, интересные кейсы, исследования и мысли вслух

https://github.com/SuperOleg39

https://twitter.com/ODrapeza

@SuperOleg39
Download Telegram
Переходим к телепортации промисов с сервера на клиент.

В первую очередь, мне очень комфортно работать с паттерном Deferred и он отлично подходит для этой задачи:


class Deferred {
constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolve = (data) => {
this.resolveData = data;
resolve(data);
};
this.reject = (reason) => {
this.rejectReason = reason;
reject(reason);
};
});
}

isResolved() {
return typeof this.resolveData !== 'undefined';
}

isRejected() {
return typeof this.rejectReason !== 'undefined';
}
}

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

Этот паттерн позволит не ждать сайд-эффект сразу, но подписаться на его выполнение позже, псевдо-код:

async function runDeferredAction() {
deferred = new Deferred()

// допустим этот асинхронный метод выполняется 5 секунд
someLongAction
.then(deferred.resolve)
.catch(deferred.reject)
}

async function render() {
// запускаем сайд эффекты, runDeferredAction резолвится сразу и не блокирует ответ
await promiseAll([
runAnyAction(),
runAnyAction(),
runDeferredAction()
])

// запускаем рендер в стрим
reactRenderToStream()

// ждем отложенный экшен
await deferred.promise

// телепортируем промис
...

// закрываем стрим
closeStream()
}

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

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

Для новых deferred экшенов оказалось проще все-таки использовать их напрямую для получения данных, что бы не усложнять “телепортацию” и не добавлять новых API (еще раздумываю над этим решением).

Итак, у нас есть набор страничных экшенов, некоторые из них помечены как deferred, опишу механизм телепортации:

- трамвай не ждет выполнения deferred экшенов перед рендерингом страницы на сервере
- на каждый такой экшен создается инстанс new Deferred(), который будет зарезолвлен после резолва самого экшена
- на клиент в head передается инлайн скрипт вида <script>window.DEFERRED_MAP['actionName'] = new Deferred()</script> - то есть название экшена и становится уникальным ключем и таким “мостом” между сервером и клиентом
- после резолва экшена, на клиент передается инлайн скрипт с резолвом промиса с соответствующими данными (их обернул в защиту от XSS атак) - <script>window.DEFERRED_MAP['actionName'].resolve(SANITIZED_ACTION_DATA)</script>

Таким образом, промис на клиенте будет зарезолвлен практически сразу после того как это произойдет на сервере.
👍9🔥2
Дальше перейдем к Suspense и использованию этих промисов.

У меня получилось следующее API, для экшенов добавляется параметр deferred, и появляется новый компонент Await, в который и передается соответствующий экшен:


const deferredAction = declareAction({
name: 'deferred',
async fn() {
await sleep(1000);
return { data: 'ok' };
},
deferred: true,
});

const Page = () => {
return (
<Suspense
fallback={<div>Loading...</div>}
>
<Await
action={deferredAction}
error={(error) => <div>Error: {error.message}</div>}
>
{(data) => <DataCmp data={`Response: ${data.data}`} />}
</Await>
</Suspense>
);
};

Page.actions = [deferredAction];

Задача Await компонента - выкидывать промис соответствующего полученному экшену Deferred объекта, до его резолва или реджекта, и код получается достаточно простой, минимальный пример без обработки ошибок, с использованием паттерна render props:

const Await = ({ action, children }) => {
const deferred = deferredMap.get(action.name)

if (deferred.isResolved()) {
return children(deferred.resolveData)
}

throw deferred.promise
}

В этом примере deferredMap на сервере обычный new Map(), а на клиенте объект который смотрит в window.DEFERRED_MAP, оба варианта имплементируют один и тот же интерфейс, что делает компонент Await универсальным.

Все остальное за нас делает React.
🔥11👍1
Теперь фреймворк умеет в потоковый рендеринг, появились deferred экшены, и Await компонент для использования этих экшенов вместе с Suspense.

Большая часть работы сделана, проблемы как всегда в деталях.

Тут очередная хвала React Working Group, а именно гайду по миграции для авторов библиотек - https://github.com/reactwg/react-18/discussions/114 - этот гайд так или иначе затрагивает все проблемы с которыми я столкнулся.

Первое, tramvai использует defer скрипты в head теге - в обычном случае как мне кажется они имеют все преимущества перед async скриптами.

Но у нас потоковый рендер, и полная загрузка страницы откладывается до завершения стрима - это значит что и defer скрипты не будут выполнены до этого момента!
А это полностью ломает преимущества Selective Hydration - новое API hydrateRoot умеет гидрировать полученную разметку по частям, по мере поступления в потоке.

Стриминговый рендеринг я уже закрыл за флагом, за этим же флагом изменил добавление скриптов с параметром async вместо defer.

Следующее, мы не можем запускать гидрацию до того как Реакт отдал application shell, то есть если наши скрипты будут выполнены и запустят гидрацию пока <APP /> пустой, очевидно получим ошибку.

Это решается с помощью bootstrapScriptContent / bootstrapScripts опций у метода renderToPipeableStream, код для инициализации гидрации будет добавлен ровно в тот момент, когда на клиент передали основную часть разметки и можем начать гидрацию.

Далее, у нас могут быть lazy компоненты (loadable) внутри Suspense и Await.

Для корректной гидрации этих компонентов, их JS и CSS файлы надо инжектить в стрим, и делать это до инжекта разметки соответствующих компонентов, и эти ресурсы должны быть загружены блокирующим браузер образом.
CSS через link и так загружается синхронно, JS скрипт должен также быть загружен синхронно, без async или defer атрибутов.
В принципе с помощью ChunkExtractor от loadable это решается без проблем, главное не задублировать уже отправленные ассеты. Добавил эту логику в стрим HtmlWritable из пары постов выше.

Итоговый HTML для наглядности в отдельном посте.

Также еще нужно учесть SPA-переходы, сериализацию объекта Error, дедупликацию работы с Deferred объектами и другие небольшие нюансы.
🔥7👍2
🔥11👍3
Долгожданный ответ на главный вопрос - нет, трамвай на Bun не заводится 😥
😱16😁4😢2
Привет!

Релизнули экспериментальный функционал с Deferred экшенами.

Сделал темплейт где можно потыкать результат - https://codesandbox.io/p/sandbox/tramvai-deferred-actions-s95q62

Экшен выполняется 1 секунду, время рендера компонента который зависит от данных замеряю через performance.mark

Deferred экшен - https://s95q62-3000.csb.app/deferred/ - время до рендера 1.3 / 1.4 секунды

Клиентский экшен - https://s95q62-3000.csb.app/non-deferred/ - время до рендера 2 секунды (а обычный экшен вообще 2.5 секунды, так как зря ждет таймаут на сервере в 500мс для любого экшена)

Чем дольше выполняется запрос, чем хуже интернет, тем быстрее пользователь должен получать результат если сравнивать deferred и обычные экшены.
🔥2👍1
Скриншоты с Performance вкладки, из интересного с deferred - позже заканчивается стрим и срабатывает событие DOMContentLoaded, как раз почему defer скрипты пришлось заменить на async
👍11
Привет!

Интересный обзор изменений в Svelte 4 в формате интервью - https://www.youtube.com/live/AOXq89h8saI

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

Рич разрабатывает либу https://github.com/Rich-Harris/dts-buddy - по сути бандлер для деклараций типов, .d.ts файлов:
- склеивает один .d.ts на основе указанной точки входа
- тришейкает внутренние интерфейсы
- генерирует source-maps .d.ts.map

Какие проблемы это решает:
- уменьшается размер пакета который надо скачивать пользователю
- TS не пытается подсказать какие-то приватные или не подходящие интерфейсы
- переходя по cmd+click на определение метода, мы попадаем в исходники, а не в не очень полезный .d.ts файл!

Сам бандлинг использует конструкцию declare module "library/sub/folder", которая работает по сути как "exports" но для тайпчекера, позволяет объявить явно только публичное API библиотеки.

Сурсмапы для .d.ts указывают на JS исходники - но это уже вроде как связано с тем что Svelte переписывают на JS + JS Doc

В любом случае даже как-то не задумывался про такую возможность. Нагуглил флаг declarationMap - но мапа будет указывать только на TS исходники судя по всему.

Как обычно много о чем подумать после видео с участием Рича Харриса, Рич крутой.
👍13💩1
Привет!

Порекламирую блог и канал моего коллеги Андрея Марченко.

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

Сразу врывается с интересной статьей про Rate Limiting (я кстати писал про трамвайный rate limiter и его как раз писал Андрей, тот случай когда шарить за алгоритмы очень полезно :) ) и готовым open source пакетом и адаптерами для fastify, nest.js и express:

- канал - https://t.me/andrey_marchenko_notes
- статья в блоге - https://amarchenko.dev/blog/2023-09-23-rate-limiting/
- и сама либа - https://github.com/Tom910/rate-limit-guard

Рекомендую, Андрей кладезь полезной информации)
👍12🔥62
Привет!

Многим из вас знакомы такие крутые фронтовые песочницы (а уже наверное полноценные облачные среды разработки) как CodeSandbox и StackBlitz.

StackBlitz на мой взгляд создали революционную технологию - Web Containers - возможность запускать Node.js в браузере.

Отдельная история, что tramvai там пока не заводится.

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

В этом году и CodeSandbox догнали конкурента, в Sandpack 2.0 появился Node.js рантайм для браузеров.

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

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

И про что я давно хочу рассказать, коротко (потому что не силен в теме), какие крутые технологии в CodeSandbox под капотом.

CodeSandbox умеет очень быстро создать для вас новую песочницу, и это окружение будет иметь неплохие ресурсы - 2 ядра и 2gb памяти.

При этом, повторные запуски этой песочницы будут ну очень быстрыми! Проверил сейчас на шаблоне tramvai deferred actions, который давно не открывал, буквально секунды, даже webpack кэши сработали на сборку dev сервера)

Как можно для огромного количества бесплатных пользователей выделять такие ресурсы? Ответ по большей части есть в этой статье - https://codesandbox.io/blog/how-we-clone-a-running-vm-in-2-seconds, ее коротко и разберу.

Кстати, интересный момент, проходил собес в CodeSandbox полтора года назад примерно, спрашивал у них как они вообще масштабируются, и интервьюер рассказал что буквально после этого собеса у него будет встреча по обсуждению решений, который помогут масштабировать контейнеры, ну и минусы Web Containers еще обсудили - я как раз уже тогда попробовал трамвай на StackBlitz завести и получил первую ошибку)

И получается в итоге ребята пришли к классным решениям!

Первое, это MicroVM Firecracker от Amazon, легковесная виртуальная машина со всеми преимуществами обычных VM (которыми я почти не успел попользоваться), запускается намного быстрее чем VM - в статье пример в 300мс против 5 секунд у VM.

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

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

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

Очень рекомендую прочитать статью, там рассказано про различные челленджы при создании снэпшота памяти контейнера, и как этот процесс ускоряли.

И вижу новую и еще более низкоуровневую статью, про несколько проблем этого решения и как с ними справились (в том числе слишком большое количество операций записи снэпшотов на диск, про системный fork) - https://codesandbox.io/blog/cloning-microvms-using-userfaultfd, звучит сложно но интересно, добавил в закладки.

В итоге, у CodeSandbox есть и нода в браузере, и контейнеры которые позволяют запустить помимо ноды пачку других языков, например Rust или Python, и которые действительно быстро работают.

Пишите в комментарии ваши мысли, и личный опыт использования этих двух инструментов!
👍204🔥1
Привет!

У Next.js недавно появилась новая экспериментальная фича - Partial Prerendering, которая решает проблемы SSR (долгий ответ сервера) и ISR (статичные данные без стриминга), и по сути комбинирует эти подходы.

С момента анонса очень хочу разобраться как же это работает под капотом, все изменения в принципе в одном PR, но далеко пока не ушел, тут изначально надо понимать как работает новый App роутер в Next.js и как работают React Server Components.

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

Что бы разобраться с partial prerendering (далее ppr), стоит рассмотреть важные моменты работы (как я их понимаю) про App роутер и RSC:

- RSC механизм отправляет на клиент данные для рендеринга в специальном текстовом формате, они могут содержать React дерево с данными, необходимыми для рендеринга, и например также JS/CSS чанки, которые надо вставить на страницу перед рендерингом, ReactDOM уже умеет с этим работать
- App роутер позволяет собирать страницы из кусочков - каждый сегмент урла может иметь отдельный лэйаут и компонент страницы
- App роутер реализует RSC механизм, он используется для отложенной загрузки данных (async компоненты) и SPA-переходов
- RSC для deferred данных - отдается в текущем HTML стриме
- RSC для SPA-переходов - отдается через отдельный GET запрос на тот же урл что и целевой роут, но с query параметром ?rsc=xxx

Также, это немного рассматривал в постах про Deferred Actions, у потокового рендеринга в React есть разделение разметки на App Shell и динамическую.

App Shell - это все ваше приложение, но с отрендеренными fallback компонентами для всех Suspense границ (внутри которых используются async серверные компоненты или выкидывается промис если у вас не Next.js, далее это будут suspended компоненты)

Динамическая часть - это то что вернут suspended компоненты после резолва промиса, у некста это как раз RSC.

PPR - это механизм предварительного рендеринга App Shell страниц приложения в билд тайме или в рантайме через ISR.
И если я все правильно понял, киллер фича тут как раз в том, что Next.js умеет в стриме ответа скомбинировать готовый App Shell, отдать его сразу первым чанком в стриме и не рендерить эти React компоненты повторно, и дальше в стриме рендерить только suspended компоненты, и вклеивать их в нужные слоты в App Shell на клиенте.

За счет вложенного роутинга и всех наворотов Next.js с кэшами, это фактически полноценный кэш рендера HTML на уровне компонентов (механизм с которым я экспериментировал - https://t.me/super_oleg_dev/125 - без особого успеха и гораздо менее перспективным образом).

То есть это и экономия на серверных вычислениях, и отличный TTFB на первый заход пользователя (стандартный паттерн с App Shell требует использования Service Worker, что несет свои сложности и работает только на повторные заходы).

Но, это подчеркивают коллеги из Vercel в твитах вокруг анонса PPR, эта фича требует очень вдумчивого подхода, то есть весь флоу что будет App Shell а что динамикой, надо тщательно проектировать.

В первую очередь опасность в возможности закэшировать динамические данные.
Next.js будет автоматически определять динамический ли компонент, например по прямому использованию глобальных headers / cookies, но мне не кажется такой механизм очень надежным.

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

И конечно официальная демка - https://www.partialprerendering.com/
👍182🔥1
В комментариях к предыдущему посту подсветили тонкости работы React Server Components, что React делает основную работу с RSC (а не Next.js как я писал ранее) и хочется копнуть еще поглубже.

Сразу прикреплю ссылку на статью где хорошо и подробно разбирается механизм RSC (upd. местами out-of-date но концепции актуальны) - https://www.plasmic.app/blog/how-react-server-components-work

Что нам тут важно и еще не рассмотрели, это пакет react-server-dom-webpack - на сервере, импорт клиентского компонента заменяется на специальный сериализуемый объект, где присутствует ссылка на модуль вида `./src/ClientComponent.client.js`.

Для клиентского кода этот пакет содержит метод для реконструкции RSC формата в дерево React элементов.

Также, в имплементации Partial Prendering можно увидеть не один раз слово postponed.

По этому ключу вижу PR в React с Postpone API (механизм для работы с промисами в компонентах, которые могут быть зарезолвлены позже, по сути defer) и PR где мы видим работу над пререндером и возобновлением рендеринга, новые апишки prerender и resume - в общем много интересных и сложных вещей на которые полагается Next и будут полагаться другие мета-фреймворки.

Собственно вот и код в Next где мы видим использование prerender метода и получение postponed данных, и рядом код с использованием resume метода который работает как раз с postponed компонентами.

postponed данные, возвращаемые методом prerender, содержат строку в формате RSC.

Итого, как работает PPR в Next.js:

На этапе сборки, для каждого роута Next вызывает react-dom/static prerender (этот метод возвращает и AppShell и postponed дерево), и сохраняет полученную разметку в кэш. Также, в кэш сохраняется и полученное на этом этапе postponed RSC дерево компонентов, если вы будете собирать официальную демку, найдете postponed в кэшах с мета информацией о страницах, например файл .next/server/app/index.meta.

В рантайме, Next проверяет есть ли страница (App Shell) в кэше, и если есть - отдает в стриме.

Затем, проверяет есть ли postponed дерево в кэше, если есть - комбинирует стрим ответа со стримом, который вернет react-dom/static resume(postponed).

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

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

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

Очень ждем, когда все эти механизмы выйдет из экспериментального статуса)
🔥5
SuperOleg dev notes pinned «Привет! Не так давно на Github была заблокирована организация Тинькофф, и к сожалению публичное зеркало репозитория tramvai со всеми настройками и звездочками кануло в лету. С опозданием переехали в новую организацию - https://github.com/tramvaijs/tramvai…»
Forwarded from Alexey Ryazanov
В 3 версии Tramvai появилась поддержка View Transitions API. Вкратце – это возможность создания анимированных переходов между состояниями DOM. Мы сделали демо на CodeSandbox, показывающую как это работает. Приглашаем ознакомиться.

Возможно, стоит показать это вашим дизайнерам, у них могут возникнуть интересные идеи 🌚
🔥16👍2
Привет!

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

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

Сам проект на начало января 2024 года еще на этапе проектирования, поэтому эта серия постов скорее всего часто будет пополняться.

Итак, про проектирование.

Все начинается с проблемы.

Во-первых, у нашего фреймворка Tramvai есть свое решение для микрофронтендов, Child Apps, использование которых только набирает обороты в компании.

Также, в Тинькофф есть еще не меньше 4-х взрослых экосистем с микрофронтендами, про это есть разные доклады и статьи, вот некоторые из них:

- https://www.youtube.com/watch?v=adgUumoPv6o
- https://habr.com/ru/companies/tinkoff/articles/517230/
- https://habr.com/ru/companies/oleg-bunin/articles/718302/

Как минимум в двух из этих экосистем существуют мощные конструкторы страниц на основе микрофронтов-виджетов.

У разных экосистем по большей части разные проблемы, например команде которая начнет использовать наши Child Apps сейчас надо самостоятельно решить и автоматизировать ряд вопросов:

- Версионирование микрофронта
- Публикация статических файлов микрофронта в s3 плюс настроенный CDN
- Управление версиями микрофронтов в приложении (релизы и откаты, история изменений)
- Витрина для отдельных микрофронтов
- Опционально, контракты между микрофронтом и приложением (входящие и исходящие данные, события)
👍2🔥21
И тут постепенно появляется идея.

Сначала я разобрал основные слабые места Child Apps, и думал про полноценную экосистему для этих микрофронтов, которая будет решать все значимые проблемы.

А что если пойти дальше, и сделать отдельный сервис - платформу для микрофронтендов, которая будет решать проблемы и других экосистем (эти проблемы обсудим далее)?

Таким образом решение проблемы разделяется на два отдельных эпика:

- Общая платформа для микрофронтов для решения популярных проблем, не зависимая от фреймворков и экосистем
- Доработки специфичные для Child Apps (в ближайших постах не вижу смысла это рассматривать)

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

RFC детально рассматривает платформу, которая состоит из следующих частей:

- Описание основных процессов (по сути одновременно и функциональные требования)
- Непосредственно сервис - бэкенд и база данных
- UI - админка сервиса (важная часть, так как управление версиями микрофронтов у многих команд либо происходит через коммиты в Gitlab, либо ручное редактирование JSON файлов, оба варианта не удобны по ряду причин)
- CLI утилита - для удобной работы с бэкендом в CI/CD пайплайнах
- Механизм контрактов
- Переиспользуемые CI/CD пайплайны с настроенными процессами

Примеры процессов - публикация новой версии микрофронта на платформе; раскатка новой версии микрофронта в приложении, и так далее.

В работе с этим RFC для себя выделил несколько слабых моментов:

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

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

Одна из основных общих проблем между экосистемами - это управление версиями микрофронтов в приложениях. На втором месте - валидация контрактов между микрофронтами и приложением - так как независимые от приложения релизы это одна из главных фич микрофронтов, тут очень важна прямая и обратная совместимость.
👍3🔥2
Структура RFC - наглядна проблема, начал с описания процессов, затем внутри них добавил детали реализации отдельных компонентов системы. Но сил все это переписывать уже не нашел)
И для лучшего восприятия идеи, пример (пре-альфа версия :) ) как может выглядеть UI платформы
4👍1