SuperOleg dev notes
Привет! Не так давно писал про механизм lazy hydration - https://t.me/super_oleg_dev/102, основанный на хаке с dangerouslySetInnerHTML Встретил интересный баг: - если LazyRender обернут в Suspense - и мы ловим ошибку This Suspense boundary received an update…
Интересно, что в нашем случае ошибка и очистка компонента Footer происходит из-за ошибок гидрации компонента страницы.
Пример лэйаута для наглядности:
Это можно исправить, обернув компонент страницы в Suspense, и Реакт свичнется на клиентский рендер только для этого поддерева компонентов, и это не затронет соседний Footer.
Но в этом случае, а у нас на сервере используется renderToString, и если Page компонент упадет с настоящей ошибкой - мы ее на сервере не перехватим и не сможем отдать 500 ошибку.
Писал про это в одном из постов, а также почему не используем на сервере апи Реакта для рендеринга в стрим:
- https://t.me/super_oleg_dev/69
- https://t.me/super_oleg_dev/49
Так и не нашел хорошего решения проблемы, кроме как избавляться на месте от конкретных ошибок гидрации.
Пример лэйаута для наглядности:
<Header />
<Page /> // hydration missmatch
<LazyRender>
<Footer />
</LazyRender>
Это можно исправить, обернув компонент страницы в Suspense, и Реакт свичнется на клиентский рендер только для этого поддерева компонентов, и это не затронет соседний Footer.
Но в этом случае, а у нас на сервере используется renderToString, и если Page компонент упадет с настоящей ошибкой - мы ее на сервере не перехватим и не сможем отдать 500 ошибку.
Писал про это в одном из постов, а также почему не используем на сервере апи Реакта для рендеринга в стрим:
- https://t.me/super_oleg_dev/69
- https://t.me/super_oleg_dev/49
Так и не нашел хорошего решения проблемы, кроме как избавляться на месте от конкретных ошибок гидрации.
Telegram
SuperOleg dev notes
Привет!
Про обработку ошибок рендеринга при SSR.
Сразу начну с примеров обработки таких ошибок.
Во многих мета-фреймворках (Next.js, Remix, SvelteKit, Nuxt.js) есть простой способ отобразить UI при ошибках в компонентах. Там где поддерживается вложенный…
Про обработку ошибок рендеринга при SSR.
Сразу начну с примеров обработки таких ошибок.
Во многих мета-фреймворках (Next.js, Remix, SvelteKit, Nuxt.js) есть простой способ отобразить UI при ошибках в компонентах. Там где поддерживается вложенный…
👍4
Привет!
Последнюю неделю работал над возможностью стримить ответы от медленных API с сервера на клиент - функционал идентичный связке Await + defer в Remix и добавленный уже и в SvelteKit, и даже в @tanstak/router (который кажется по прежнему очень сырой продукт)
Уже рассматривал эту фичу в предыдущих постах - https://t.me/super_oleg_dev/80
Какой кейс решает Await + defer:
- У нас есть медленное API
- Мы не хотим так медленно отвечать пользователям и вызываем запрос на клиенте
- Но от полученных данных зависит важный функционал страницы
В таком кейсе пользователь увидит контент после следующего водопада (последовательного и блокирующего) событий:
- Ответ от сервера
- Загрузка клиентских скриптов и стилей
- Гидрация приложения
- Запрос в API
- Рендер с необходимыми данными
Отложенная загрузка данных на сервере + стриминг этих данных на клиент позволяет гораздо раньше запустить запрос, и таким образом сильно уменьшить итоговое время до отрисовки важного контента (в каких-то кейсах LCP метрика может как раз отражать это время, но лучше собирать бизнесовые метрики под конкретные продуктовые фичи)
Стриминг отложенных данных возможен практически нативно начиная с 18 реакта, и состоит из нескольких компонентов:
- renderToPipeableStream - новое API для рендеринга разметки на сервере в стриме
- “Телепортация” промисов для каждого отложенного запроса с сервера на клиент
- Suspense - умеет вернуть на сервере фаллбэк для отложенного (suspended) компонента, и перерендерить его на клиенте после резолва
В ближайшей серии постов хочу рассмотреть нашу реализацию по частям, получилось больше информации чем планировал и более сумбурно + технически сложнее, пишите фидбек, если такое не сильно комфортно читать (по крайней мере в формате постов в телеге)
Последнюю неделю работал над возможностью стримить ответы от медленных API с сервера на клиент - функционал идентичный связке Await + defer в Remix и добавленный уже и в SvelteKit, и даже в @tanstak/router (который кажется по прежнему очень сырой продукт)
Уже рассматривал эту фичу в предыдущих постах - https://t.me/super_oleg_dev/80
Какой кейс решает Await + defer:
- У нас есть медленное API
- Мы не хотим так медленно отвечать пользователям и вызываем запрос на клиенте
- Но от полученных данных зависит важный функционал страницы
В таком кейсе пользователь увидит контент после следующего водопада (последовательного и блокирующего) событий:
- Ответ от сервера
- Загрузка клиентских скриптов и стилей
- Гидрация приложения
- Запрос в API
- Рендер с необходимыми данными
Отложенная загрузка данных на сервере + стриминг этих данных на клиент позволяет гораздо раньше запустить запрос, и таким образом сильно уменьшить итоговое время до отрисовки важного контента (в каких-то кейсах LCP метрика может как раз отражать это время, но лучше собирать бизнесовые метрики под конкретные продуктовые фичи)
Стриминг отложенных данных возможен практически нативно начиная с 18 реакта, и состоит из нескольких компонентов:
- renderToPipeableStream - новое API для рендеринга разметки на сервере в стриме
- “Телепортация” промисов для каждого отложенного запроса с сервера на клиент
- Suspense - умеет вернуть на сервере фаллбэк для отложенного (suspended) компонента, и перерендерить его на клиенте после резолва
В ближайшей серии постов хочу рассмотреть нашу реализацию по частям, получилось больше информации чем планировал и более сумбурно + технически сложнее, пишите фидбек, если такое не сильно комфортно читать (по крайней мере в формате постов в телеге)
❤19
Основная сложность для фреймворка tramvai в реализации этой фичи было полное отсутствие поддержки стриминга HTML разметки.
Эксперименты со стримингом были, но в целом есть ряд минусов, рассмотренных в этом посте - https://t.me/super_oleg_dev/49, из-за которых долго не развивали идею. Но концепция Await + defer очень уж хороша, что бы ее не использовать.
Плюс недавно в твиттере увидел интересную мысль, что если в целом для пользователя есть возможность благодаря стримингу сильно ускорить ответ и улучшить UX, этот пользователь в итоге принесет больше денег и возросшая сложность и нагрузка на сервера того стоит)
Итак, как сейчас работает трамвай что бы отрендерить HTML:
- Есть заранее заданная схема HTML страницы со слотами (например мета-теги, инлайн скрипты, head скрипты, body скрипты и т.д.)
- На разных этапах обработки запроса на эти слоты регистрируются соответствующие ресурсы (допустим сходили в API, добавили нужный title, инлайн-скрипт или json-ld)
- В самом конце, рендерим React компонент через renderToString, и получаем окончательный список ресурсов (до рендера не знаем ссылки на JS/CSS используемых на странице loadable компонентов)
- Склеиваем все по схеме, отдаем ответ клиенту одним чанком
Вот как выглядит флоу генерации HTML:
И для наглядности, представим схему страницы в очень упрощенном варианте:
Тут
При потоковом рендеринге, я решил не ломать полностью существующий флоу ответа, и не начинать стримить раньше, а делать это в том же месте где раньше отдавали всю страницу целиком - тогда мы будем уверены что все необходимые данные уже получены, а ресурсы зарегистрированы, и не нужно будет изобретать свои механизмы чтобы “достримить” различные теги на страницу и к примеру вставить в
Эксперименты со стримингом были, но в целом есть ряд минусов, рассмотренных в этом посте - https://t.me/super_oleg_dev/49, из-за которых долго не развивали идею. Но концепция Await + defer очень уж хороша, что бы ее не использовать.
Плюс недавно в твиттере увидел интересную мысль, что если в целом для пользователя есть возможность благодаря стримингу сильно ускорить ответ и улучшить UX, этот пользователь в итоге принесет больше денег и возросшая сложность и нагрузка на сервера того стоит)
Итак, как сейчас работает трамвай что бы отрендерить HTML:
- Есть заранее заданная схема HTML страницы со слотами (например мета-теги, инлайн скрипты, head скрипты, body скрипты и т.д.)
- На разных этапах обработки запроса на эти слоты регистрируются соответствующие ресурсы (допустим сходили в API, добавили нужный title, инлайн-скрипт или json-ld)
- В самом конце, рендерим React компонент через renderToString, и получаем окончательный список ресурсов (до рендера не знаем ссылки на JS/CSS используемых на странице loadable компонентов)
- Склеиваем все по схеме, отдаем ответ клиенту одним чанком
Вот как выглядит флоу генерации HTML:
// тут по дефолту renderToString
renderReact()
// после рендера знаем все JS/CSS которые нужны на странице
addScriptsAndStyles()
// сериализуем данные для передачи на клиент
dehydrateState()
// генерируем HTML по схеме
generateHtml()
И для наглядности, представим схему страницы в очень упрощенном варианте:
<head>
<META />
<LINKS />
<SCRIPTS />
</head>
<body>
<APP />
<TAIL_SCRIPTS />
</body>
Тут
<APP />
- это часть разметки которую рендерит React.При потоковом рендеринге, я решил не ломать полностью существующий флоу ответа, и не начинать стримить раньше, а делать это в том же месте где раньше отдавали всю страницу целиком - тогда мы будем уверены что все необходимые данные уже получены, а ресурсы зарегистрированы, и не нужно будет изобретать свои механизмы чтобы “достримить” различные теги на страницу и к примеру вставить в
<head>
после того, как уже отдали соответствующий закрывающий тег (пример проблемы)Telegram
SuperOleg dev notes
Привет!
Активно готовим tramvai к React 18, хочу поделиться важными для меня плюсами, и к сожалению минусами.
Рендеринг на сервере стал быстрее.
На одном простом демо приложении, RPS вырос с 50 до 70 просто при обновлении react и react-dom.
Т.к. именно…
Активно готовим tramvai к React 18, хочу поделиться важными для меня плюсами, и к сожалению минусами.
Рендеринг на сервере стал быстрее.
На одном простом демо приложении, RPS вырос с 50 до 70 просто при обновлении react и react-dom.
Т.к. именно…
👍6🔥6
Там где раньше мы отдавали весь HTML, я пишу в стрим первым чанком всю разметку до
Для стриминга, на каждый запрос создаю и сохраняю в DI отдельный Duplex стрим, который и отдаю в ответ через Fastify reply.send(stream). Это позволяет легко переиспользовать его в разных трамвайных модулях.
Во время отдачи первого чанка с
Еще раз подчеркну, что renderToPipeableStream приходится запускать раньше чем мы можем использовать данные из него, то есть один из основных челленджей в задаче идет из необходимости поддержать текущую архитектуру с минимумом доработок.
Для решения проблемы завел сущность
- метод для добавления асинхронной задачи (push)
- метод для запуска и ожидания всех задач (process)
Пока максимально простой, в будущем скорее всего понадобится добавлять различные приоритеты для задач, если менять текущую архитектуру со схемой HTML страницы.
Этот менеджер задач позволяет в любое время запушить в очередь таски с записью разметки в стрим, но выполнить их строго в определенный момент времени - в нашем случае после того как отдали клиенту открывающий тег
Итого, у нас примерно так выглядит ответ клиенту, после того как сгенерировали начальный HTML по схеме:
Для того что бы данные из
- Создаем кастомный Writable стрим, который и передаем в метод
- Этот стрим на каждый
- Также добавляем еще одну задачу в taskManager которая зарезолвится после события
Пример кода без лишних подробностей:
<APP />
:<head>
<META />
<LINKS />
<SCRIPTS />
</head>
<body>
...
Для стриминга, на каждый запрос создаю и сохраняю в DI отдельный Duplex стрим, который и отдаю в ответ через Fastify reply.send(stream). Это позволяет легко переиспользовать его в разных трамвайных модулях.
Во время отдачи первого чанка с
<head>
, renderToPipeableStream уже был запущен (и для сохранения существующего жизненного цикла запроса и для ускорения ответа), и тут нам важно избежать гонки, реакт должен отдавать разметку в стрим строго на слот <APP />
, после отдачи первого чанка.Еще раз подчеркну, что renderToPipeableStream приходится запускать раньше чем мы можем использовать данные из него, то есть один из основных челленджей в задаче идет из необходимости поддержать текущую архитектуру с минимумом доработок.
Для решения проблемы завел сущность
ResponseTaskManager
с такими возможностями:- метод для добавления асинхронной задачи (push)
- метод для запуска и ожидания всех задач (process)
Пока максимально простой, в будущем скорее всего понадобится добавлять различные приоритеты для задач, если менять текущую архитектуру со схемой HTML страницы.
Этот менеджер задач позволяет в любое время запушить в очередь таски с записью разметки в стрим, но выполнить их строго в определенный момент времени - в нашем случае после того как отдали клиенту открывающий тег
<body>
в первом чанке.Итого, у нас примерно так выглядит ответ клиенту, после того как сгенерировали начальный HTML по схеме:
const [headAndBodyStart, bodyEnd] = html.split('<APP />')
// передаем стрим в ответ клиенту
reply.send(stream)
// пишем первый чанк с head и открывающим body
stream.push(headAndBodyStart)
// тут выполняются задачи, поставленные во время работы renderToPipeableStream
await taskManager.process()
// пишем закрывающий body
stream.push(bodyEnd)
// завершаем ответ
stream.push(null)
Для того что бы данные из
renderToPipeableStream
писать в стрим по требованию а не по факту их получения:- Создаем кастомный Writable стрим, который и передаем в метод
pipe
в коллбэке onShellReady
(когда готова первая часть HTML но все отложенные Suspense компоненты вернули fallback)- Этот стрим на каждый
write
от реакта добавляет задачу в taskManager с записью HTML в поток- Также добавляем еще одну задачу в taskManager которая зарезолвится после события
finish
у нашего Writable стрима (когда все отложенные Suspense компоненты зарезолвлены и реакт выполнил всю работу)Пример кода без лишних подробностей:
// этот стрим откладывает запись разметки в стрим ответа
class HtmlWritable extends Writable {
_write(chunk, encoding, callback) {
// stream - это именно поток ответа клиенту с предыдущего шага
taskManager.push(() => {
stream.push(chunk)
})
callback()
}
}
// реализацию Deferred добавлю позже
const allReadyDeferred = Deferred();
// задача на ожидание окончания рендера
taskManager.push(() => {
return allReadyDeferred.promise;
})
// событие сработает когда pipe завершит работу
htmlWritable.on('finish', () => {
allReadyDeferred.resolve()
})
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
// pipe готов передавать данные
pipe(htmlWritable)
}
})
fastify.dev
Reply | Fastify
👍5
Итак, мы научились отдавать HTML в потоке и работать с renderToPipeableStream, что нам все это дает?
18 реакт умеет и отдавать разметку в потоке, и гидрировать ее на клиенте по мере поступления, вместо одной тяжелой задачи по гидрации.
Границами
Этот механизм подробно описан в посте New Suspense SSR Architecture in React 18.
Официально для нас разработчиков есть только один API, который поддерживает
Но Next.js, Relay и ряд других инструментов уже давно используют подкапотный механизм для своих апишек для загрузки данных, по сути все сводится к тому, что обернутый в Suspense механизм должен выкинуть Promise, и пока он не зарезолвлен, будет отрендерен пропс fallback.
Кастомную реализацию можно посмотреть тут - https://blog.logrocket.com/data-fetching-react-suspense/
Для SSR приложений, важный нюанс - этот Promise должен быть в одном и том же состоянии и на клиенте, и на сервере:
- Отрендерили app shell на сервере с pending промисом для Suspense компонента
- Отдали эту разметку на клиент, запустили гидрацию
- Этот Suspense компонент уже в клиентском коде на момент гидрации должен получить промис в таком же pending статусе
- Затем на сервере промис переходит в resolved, мы телепортируем его на клиент
- Клиентский Suspense видит что промис зарезолвлен и рендерит вложенный компонент
Интересный нюанс, когда renderToPipeableStream отдает app shell с фаллбэками, он добавляет пустые
18 реакт умеет и отдавать разметку в потоке, и гидрировать ее на клиенте по мере поступления, вместо одной тяжелой задачи по гидрации.
Границами
Suspense
определяется, какие компонеты будут отрендерены сразу (app shell), для каких фаллбэк (индикатор загрузки), а какие будут отрендерены после резолва Suspense
компонентов.Этот механизм подробно описан в посте New Suspense SSR Architecture in React 18.
Официально для нас разработчиков есть только один API, который поддерживает
Suspense
как асинхронное действие - это React.lazy
.Но Next.js, Relay и ряд других инструментов уже давно используют подкапотный механизм для своих апишек для загрузки данных, по сути все сводится к тому, что обернутый в Suspense механизм должен выкинуть Promise, и пока он не зарезолвлен, будет отрендерен пропс fallback.
Кастомную реализацию можно посмотреть тут - https://blog.logrocket.com/data-fetching-react-suspense/
Для SSR приложений, важный нюанс - этот Promise должен быть в одном и том же состоянии и на клиенте, и на сервере:
- Отрендерили app shell на сервере с pending промисом для Suspense компонента
- Отдали эту разметку на клиент, запустили гидрацию
- Этот Suspense компонент уже в клиентском коде на момент гидрации должен получить промис в таком же pending статусе
- Затем на сервере промис переходит в resolved, мы телепортируем его на клиент
- Клиентский Suspense видит что промис зарезолвлен и рендерит вложенный компонент
Интересный нюанс, когда renderToPipeableStream отдает app shell с фаллбэками, он добавляет пустые
template
теги и небольшой скрипт $RC
в котором вроде как подменяется fallback на разметку из темплейта, а после резолва suspended компонентов для них также отправляются на клиент скрипты с использованием этого $RC
метода - очень похоже что это логика именно для React Server Components, интересно разобраться почему оно вообще добавляется в нашем случае, когда никаких RSC нет.GitHub
New Suspense SSR Architecture in React 18 · reactwg react-18 · Discussion #37
Overview React 18 will include architectural improvements to React server-side rendering (SSR) performance. These improvements are substantial and are the culmination of several years of work. Most...
👍4🔥1
Переходим к телепортации промисов с сервера на клиент.
В первую очередь, мне очень комфортно работать с паттерном Deferred и он отлично подходит для этой задачи:
На сервере, на каждое асинхронное действие, которое мы пометим как отложенное, необходимо создать свой экземпляр Deferred промиса, и выполнить его resolve/reject по завершению соответствующей асинхронной задачи.
Этот паттерн позволит не ждать сайд-эффект сразу, но подписаться на его выполнение позже, псевдо-код:
В качестве механизма для отложенной загрузки данных показалось хорошим решением использовать уже существующие трамвайные Actions - наш основной инструмент для создания и выполнения сайд-эффектов.
В отличие от функций - загрузчиков данных в Remix или SvelteKit, на одну страницу трамвай позволяет запустить множество параллельных экшенов, также мы обычно не используем данные из экшенов напрямую, а передаем их с сервера на клиент через трамвайные сторы.
Для новых deferred экшенов оказалось проще все-таки использовать их напрямую для получения данных, что бы не усложнять “телепортацию” и не добавлять новых API (еще раздумываю над этим решением).
Итак, у нас есть набор страничных экшенов, некоторые из них помечены как
- трамвай не ждет выполнения deferred экшенов перед рендерингом страницы на сервере
- на каждый такой экшен создается инстанс
- на клиент в head передается инлайн скрипт вида
- после резолва экшена, на клиент передается инлайн скрипт с резолвом промиса с соответствующими данными (их обернул в защиту от XSS атак) -
Таким образом, промис на клиенте будет зарезолвлен практически сразу после того как это произойдет на сервере.
В первую очередь, мне очень комфортно работать с паттерном 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>
Таким образом, промис на клиенте будет зарезолвлен практически сразу после того как это произойдет на сервере.
tramvai.dev
Actions | tramvai
Explanation
👍9🔥2
Дальше перейдем к
У меня получилось следующее API, для экшенов добавляется параметр
Задача
В этом примере deferredMap на сервере обычный
Все остальное за нас делает React.
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 использует
Но у нас потоковый рендер, и полная загрузка страницы откладывается до завершения стрима - это значит что и
А это полностью ломает преимущества Selective Hydration - новое API
Стриминговый рендеринг я уже закрыл за флагом, за этим же флагом изменил добавление скриптов с параметром
Следующее, мы не можем запускать гидрацию до того как Реакт отдал application shell, то есть если наши скрипты будут выполнены и запустят гидрацию пока <
Это решается с помощью
Далее, у нас могут быть lazy компоненты (loadable) внутри Suspense и Await.
Для корректной гидрации этих компонентов, их JS и CSS файлы надо инжектить в стрим, и делать это до инжекта разметки соответствующих компонентов, и эти ресурсы должны быть загружены блокирующим браузер образом.
CSS через link и так загружается синхронно, JS скрипт должен также быть загружен синхронно, без
В принципе с помощью
Итоговый HTML для наглядности в отдельном посте.
Также еще нужно учесть SPA-переходы, сериализацию объекта Error, дедупликацию работы с Deferred объектами и другие небольшие нюансы.
Большая часть работы сделана, проблемы как всегда в деталях.
Тут очередная хвала 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 объектами и другие небольшие нюансы.
GitHub
Library Upgrade Guide: <script> (e.g. SSR frameworks) · reactwg react-18 · Discussion #114
Library Upgrade Guide: <script> This is an upgrade guide for frameworks and custom app setups that manages loading JS modules and inserting script tags. In particular if they work to support ...
🔥7👍2
Долгожданный ответ на главный вопрос - нет, трамвай на 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 и обычные экшены.
Релизнули экспериментальный функционал с 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 файл!
Сам бандлинг использует конструкцию
Сурсмапы для .d.ts указывают на JS исходники - но это уже вроде как связано с тем что Svelte переписывают на JS + JS Doc
В любом случае даже как-то не задумывался про такую возможность. Нагуглил флаг declarationMap - но мапа будет указывать только на TS исходники судя по всему.
Как обычно много о чем подумать после видео с участием Рича Харриса, Рич крутой.
Интересный обзор изменений в 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 исходники судя по всему.
Как обычно много о чем подумать после видео с участием Рича Харриса, Рич крутой.
YouTube
Dev Vlog: June 2023 - Svelte 4.0 with Rich Harris
Channel Sponsored by Vercel - https://bit.ly/3SkkVNb
0:00 Warmup
1:45 Svelte 4 Beta
2:30 Migration guide
27:50 New stuff
36:12 Internal changes
54:27 New website
57:32 Q&A
0:00 Warmup
1:45 Svelte 4 Beta
2:30 Migration guide
27:50 New stuff
36:12 Internal changes
54:27 New website
57:32 Q&A
👍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
Рекомендую, Андрей кладезь полезной информации)
Порекламирую блог и канал моего коллеги Андрея Марченко.
Андрей большую часть моей карьеры в Тинькофф был моим лидом и наставником, а сейчас работает в 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
Рекомендую, Андрей кладезь полезной информации)
Telegram
Андрей Марченко Dev заметки
Мои @tom910 заметки о IT, Node.js, Web и процессах
👍12🔥6❤2
Привет!
Многим из вас знакомы такие крутые фронтовые песочницы (а уже наверное полноценные облачные среды разработки) как 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, и которые действительно быстро работают.
Пишите в комментарии ваши мысли, и личный опыт использования этих двух инструментов!
Многим из вас знакомы такие крутые фронтовые песочницы (а уже наверное полноценные облачные среды разработки) как 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, и которые действительно быстро работают.
Пишите в комментарии ваши мысли, и личный опыт использования этих двух инструментов!
Stackblitz
Introducing WebContainers: Run Node.js natively in your browser
Today we're excited to announce WebContainers, a new type of WebAssembly-based operating system that boots instantly and enables Node.js environments to run natively in-browser.
👍20❤4🔥1
Ну очень крутые теория, визуализация и примеры про CRDTs (conflict-free replicated data types) - https://jakelazaroff.com/words/an-interactive-intro-to-crdts/
Ссылочка из рассылки Architecture Weekly - https://twitter.com/vvsevolodovich/status/1713492388188921960
Ссылочка из рассылки Architecture Weekly - https://twitter.com/vvsevolodovich/status/1713492388188921960
Jakelazaroff
An Interactive Intro to CRDTs | jakelazaroff.com
CRDTs don't have to be all academic papers and math jargon. Learn what CRDTs are and how they work through interactive visualizations and code samples.
👍7🔥2
Привет!
У 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 параметром
Также, это немного рассматривал в постах про 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/
У 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/
Telegram
SuperOleg dev notes
Привет!
Не так давно писал про механизм lazy hydration - https://t.me/super_oleg_dev/102, основанный на хаке с dangerouslySetInnerHTML
Встретил интересный баг:
- если LazyRender обернут в Suspense
- и мы ловим ошибку This Suspense boundary received an update…
Не так давно писал про механизм lazy hydration - https://t.me/super_oleg_dev/102, основанный на хаке с dangerouslySetInnerHTML
Встретил интересный баг:
- если LazyRender обернут в Suspense
- и мы ловим ошибку This Suspense boundary received an update…
👍18❤2🔥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 - на сервере, импорт клиентского компонента заменяется на специальный сериализуемый объект, где присутствует ссылка на модуль вида `
Также, в имплементации Partial Prendering можно увидеть не один раз слово postponed
Собственно вот и код в Next где мы видим использование prerender метода и получение postponed данных, и рядом код с использованием resume метода который работает как раз с postponed компонентами.
postponed данные, возвращаемые методом prerender, содержат строку в формате RSC.
Итого, как работает PPR в Next.js:
На этапе сборки, для каждого роута Next вызывает
В рантайме, Next проверяет есть ли страница (App Shell) в кэше, и если есть - отдает в стриме.
Затем, проверяет есть ли postponed дерево в кэше, если есть - комбинирует стрим ответа со стримом, который вернет
Концептуально понятно и просто, изящно, и тут очень радует что большую часть работы все-таки делает фреймворк - React.
Конечно, у Next тут много своего кода и специфики, например как собирается дерево компонентов страниц и лэйаутов для рендеринга из сегментов, но никакой магии с вычислением отдельного отложенного React дерева на этом уровне нет.
До этого у меня был стереотип что React не будет так сильно залазить в сборку / серверную часть и гораздо больше всего остается на плечах мета-фреймворков.
Очень ждем, когда все эти механизмы выйдет из экспериментального статуса)
Сразу прикреплю ссылку на статью где хорошо и подробно разбирается механизм 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 не будет так сильно залазить в сборку / серверную часть и гораздо больше всего остается на плечах мета-фреймворков.
Очень ждем, когда все эти механизмы выйдет из экспериментального статуса)
GitHub
Add Postpone API by sebmarkbage · Pull Request #27238 · facebook/react
This adds an experimental unstable_postpone(reason) API.
Currently we don't have a way to model effectively an Infinite Promise. I.e. something that suspends but never resolves. The reason ...
Currently we don't have a way to model effectively an Infinite Promise. I.e. something that suspends but never resolves. The reason ...
🔥5
И еще несколько более актуальных и подробных ссылок по RSC от @popuguy:
- https://timtech.blog/posts/react-server-components-rsc-no-framework/
- https://demystifying-rsc.vercel.app/
- https://timtech.blog/posts/react-server-components-rsc-no-framework/
- https://demystifying-rsc.vercel.app/
Tim's Tech Blog
React Server Components, without a framework?
Can we use React Server Components today, without a framework like Next.js? As our experiment shows, not without significant challenges - for now at least?
👍4