TGViewer
Channel Public Channel
SuperOleg dev notes

SuperOleg dev notes

@super_oleg_dev

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

https://github.com/SuperOleg39

https://twitter.com/ODrapeza

@SuperOleg39
Subscribers
1.75K
Photos
57
Videos
3
Links
160

Showing posts older than #142 · Back to latest

Older Posts 19 shown
Post #141 1.47K
Привет!

Многим из вас знакомы такие крутые фронтовые песочницы (а уже наверное полноценные облачные среды разработки) как 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
Post #140 1.43K
Привет!

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

Андрей большую часть моей карьеры в Тинькофф был моим лидом и наставником, а сейчас работает в 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
Post #139 1.47K
Привет!

Интересный обзор изменений в 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 - YouTube Enjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on YouTube.
  • 👍 13
  • 💩 1
Post #137 1.29K
Скриншоты с Performance вкладки, из интересного с deferred - позже заканчивается стрим и срабатывает событие DOMContentLoaded, как раз почему defer скрипты пришлось заменить на async
  • 👍 11
Post #136 1.37K
Привет!

Релизнули экспериментальный функционал с 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
Post #135 1.16K
Долгожданный ответ на главный вопрос - нет, трамвай на Bun не заводится 😥
  • 😱 16
  • 😁 4
  • 😢 2
Post #134 1.56K
  • 🔥 11
  • 👍 3
Post #133 1.6K
Теперь фреймворк умеет в потоковый рендеринг, появились 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 объектами и другие небольшие нюансы.
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
Post #132 1.05K
Дальше перейдем к 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
Post #131 814
Переходим к телепортации промисов с сервера на клиент.

В первую очередь, мне очень комфортно работать с паттерном 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
Post #130 833
Итак, мы научились отдавать HTML в потоке и работать с renderToPipeableStream, что нам все это дает?

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
Post #129 841
Там где раньше мы отдавали весь HTML, я пишу в стрим первым чанком всю разметку до <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 Fastify - Fast and Low Overhead Web Framework Fastify is a fast and low overhead web framework for Node.js.
  • 👍 5
Post #128 857
Основная сложность для фреймворка 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:

// тут по дефолту 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. Т.к. именно…
  • 👍 6
  • 🔥 6
Post #127 931
Привет!

Последнюю неделю работал над возможностью стримить ответы от медленных 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
Post #126 1.1K
SuperOleg dev notes Привет! Не так давно писал про механизм lazy hydration - https://t.me/super_oleg_dev/102, основанный на хаке с dangerouslySetInnerHTML Встретил интересный баг: - если LazyRender обернут в Suspense - и мы ловим ошибку This Suspense boundary received an update…
Интересно, что в нашем случае ошибка и очистка компонента Footer происходит из-за ошибок гидрации компонента страницы.

Пример лэйаута для наглядности:

<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 при ошибках в компонентах. Там где поддерживается вложенный…
  • 👍 4
Post #125 1.09K
Привет!

Не так давно писал про механизм lazy hydration - https://t.me/super_oleg_dev/102, основанный на хаке с dangerouslySetInnerHTML

Встретил интересный баг:
- если LazyRender обернут в Suspense
- и мы ловим ошибку This Suspense boundary received an update before it finished hydrating
- то React очищает HTML в LazyRender, который пришел от сервера

Думаю такая комбинация это большая редкость, но было интересно)

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

Upd.

Проблема в целом кажется при любых ошибках гидрации выше уровнем(
Telegram SuperOleg dev notes Привет! Одна из первый идей, которую я реализовывал в Тинькофф - ленивая гидрация для React компонентов. Ленивая гидрация позволяет не выполнять реакту код компонента на клиенте сразу при гидрации всего приложения, и отлично комбинируется с IntersectionObserver…
  • 🤔 2
  • 🔥 1
Post #124 1.42K
Привет!

Недавно актуализировали список поддерживаемых браузеров на tinkoff.ru, и немного погрузился в экосистему вокруг, а именно в взаимосвязь следующих инструментов:

- browserslist
- Can I Use
- core-js
- @babel/preset-env

Если коротко, как это устроено:

core-js поставляет полифиллы для различных ECMAScript фич, и начиная с 3 версии поддерживает свою таблицу совместимости фич и браузеров - http://zloirock.github.io/core-js/compat/

@babel/preset-env добавляет нужные полифиллы из core-js при транспиляции кода, до версии 7.3 собирал данные по совместимости из https://kangax.github.io/compat-table/es6/, минусы описаны в “core-js@3, babel and a look into the future”, с 7.4 использует данные core-js.

browserslist позволяет шарить между всеми инструментами общий список браузеров, и генерировать его по различным условиям типа “> 1%” или “not dead”.

Также browserslist использует сервис Can I Use в качестве источника данных о названиях и версиях браузера, актуальный список можно посмотреть тут - https://caniuse.com/usage-table.

Есть кстати не оч подробная и свежая, но неплохая статья про preset-env - https://www.jnielson.com/demystifying-babel-preset-env

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

1. Данные Can I Use ограничены - не знаю как они собираются, но к примеру по популярным в РФ или в Китае браузерам может быть очень мало статистики, и они будут отсутствовать, либо у браузера в списке будет одна из свежих версий. Пример проблемы - https://github.com/babel/babel/issues/8545

2. Compat данные core-js ограничены - к примеру для поддержки браузера Samsung в core-js добавляли таблицу - маппинг версий Samsung на используемый в них движок Chromium. Yandex или UC браузеров там нет, причем достаточно давно https://github.com/zloirock/core-js/issues/721. Пример проблемы в preset-env - https://github.com/browserslist/browserslist/issues/290

Как нас (и возможно вас) задевают эти проблемы - мы используем browserslist для нескольких кейсов:
- поддержка необходимых браузеров, это сам @babel/preset-env и наш механизм генерации полифиллов
- проверка браузера на попадание в наш список современных браузеров, для таких отдадим modern сборку, в которой значительно меньше кода
- проверка на устаревший браузер, который ниже нашего дефолтного списка, для таких отдаем заглушку с просьбой обновить браузер

С modern сборкой все хорошо - если список, есть браузеры выше чем версии из списка, парсим User-Agent или Client Hints и отдаем подходящий код.

Но гарантировать поддержку браузеров мы получается просто не можем!

Допустим нам надо поддержать UC браузер от 11 версии и Samsung от 2 версии, потому что с них есть стабильный поток пользователей.

Но в свежих данных от https://caniuse.com/usage-table таких старый версий просто нет - то есть и подобрать соответствие ES фич не к чему. А в случае с UC браузером у нас и compat данных в core-js нет, для Samsung есть хотя бы маппинг к Chromium.

Также, проблема с проверкой на устаревший браузер. Приходит пользователь с UC браузера 11 версии, которая есть в нашем списке, но после обработки списка через browserslist, отдается минимально известный в Can I Use - 15 версии (я такого даже не нагуглил).

В итоге все пользователи с 11, 12 и 13 версий UC браузера получат редирект на заглушку, и мы об этом просто так не узнаем.

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

Вот такой дивный мир) Утешает только то, что с достаточно старыми версиями хрома и при наличии Android Browser в списках, preset-env добавит практически все популярные полифиллы.

Пишите в комментарии ваш опыт с этими инструментами, и обязательно поправьте если где-то ошибся.
GitHub core-js/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md at master · zloirock/core-js Standard Library. Contribute to zloirock/core-js development by creating an account on GitHub.
  • 👍 11
  • ❤ 2
Post #123 1.47K
Привет!

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

С опозданием переехали в новую организацию - https://github.com/tramvaijs/tramvai

Публикация в npm снова работает, и свежая документация доступна на https://tramvai.dev/

Также, периодически поступали вопросы в личку по поводу фреймворка, решил что удобнее будет завести отдельный чат - https://t.me/tramvaijs

Приглашаю вас в чат, обсуждать любые вопросы про tramvai, и общие темы вокруг серверного рендеринга и мета-фреймворков.
GitHub GitHub - tramvaijs/tramvai: A modular framework for universal JS applications A modular framework for universal JS applications. Contribute to tramvaijs/tramvai development by creating an account on GitHub.
  • 😢 17
  • 🤯 9
  • 👍 1
  • 😁 1
Post #122 1.44K
Привет!

Заметка про Client Hints API

Переходили на это API на сервере, с фаллбэком на парсинг User-Agent через ua-parser-js, как было раньше.
На сервере это HTTP заголовок sec-ch-ua который легко разобрать.

Также недавно интегрировали на клиенте, там данные доступны в navigator.userAgentData.

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

Столкнулся с проблемой мокирования User-Agent и взаимодействия с этой апишкой в следующих кейсах:
- в Chrome Devtools при эмуляции мобильных устройств
- в Playwright при использовании кастомного User-Agent

Проблема следующая - navigator.userAgentData не синхронизируется с navigator.userAgent:
- для устройств с Chrome и Android, где поддержка Client Hints есть, пишет реальную версию текущего браузера, хотя в UA условно 80й хром
- для IOS + Safari - вместо undefined, как должно быть если апишка не поддержкивается, возвращает бесполезный объект с пустым массивом brands и пустой строкой в platform

Пример проблемы - https://github.com/microsoft/playwright/issues/14361

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

Надеюсь будет полезно!
MDN Web Docs HTTP Client hints - HTTP | MDN Client hints are a set of HTTP request header fields that a server can proactively request from a client to get information about the device, network, user, and user-agent-specific preferences. The server can determine which resources to send, based on the…
  • 👍 7
Older posts →
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →