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

https://github.com/SuperOleg39

https://twitter.com/ODrapeza

@SuperOleg39
Download Telegram
Привет!

Столкнулись с интересным кейсом, для меня это просто утечка памяти года - JSON.parse(undefined)

Этот простой код в Node.js до 18 версии вызывает небольшую утечку памяти, примерно 128 байт на вызов, которые не очищаются с помощью Garbage Collector.

Проблема описана в этом issue, и как будто бы исправлена в актуальных мажорках ноды, но фактически утечка присутствует в 14.x и 16.x версиях, по результатам наших проверок.

Почему утечка года:
- она маленькая и медленная, нужны хорошие нагрузки что бы была заметна
- совершенно неожиданная при профилировании (просто посмотрите на этот "undefined" на скриншоте, я просто игнорировал его когда встречал)
- очень легко воспроизвести, у нас оказалось несколько мест где парсим те cookies, которые часто undefined

Очень рад работать с такими крутыми коллегами, один из которых смог это раскопать)

Основной причиной, почему начали смотреть утечки памяти, оказался другой код, связанный с LRU кэшами, но в любом случае это хороший повод обновить Node.js
🔥25
С наступающим, ребята!
Спасибо за ваш интерес к каналу, возможность делиться мыслями вслух на такую аудиторию - очень волнительно и бесценно ❤️
🎉194👍2
Привет!

Давно хотел поделиться накопленным за последний год опытом оптимизаций и масштабирования SSR приложений.

Думал уложиться в несколько telegram постов, но меня немножечко прорвало, и получилась почти полноценная статья.

В статье расскажу про основные проблемы SSR (спойлер - React и Node.js) и рассмотрю ряд возможных оптимизаций:

- Static Site Generation
- Rendering at the Edge
- Микросервисы
- Оптимизациия кода
- Кэширование компонентов
- Кэширование запросов
- Rate Limiting
- Fallback кэш страниц
- Client-side rendering fallback
- Кластеризация и воркеры

Ссылка на статью в моем Notion - https://superoleg39.notion.site/SSR-04ad1ab46de346edb244a1112bd357a3

Очень жду ваш фидбек в комментарии)
🔥35👍5👏1
Привет!

Про обработку ошибок рендеринга при SSR.

Сразу начну с примеров обработки таких ошибок.

Во многих мета-фреймворках (Next.js, Remix, SvelteKit, Nuxt.js) есть простой способ отобразить UI при ошибках в компонентах. Там где поддерживается вложенный роутинг, этот error компонент можно добавлять на уровне конкретных страниц, и таким образом рендерить error UI локально (поиграться с примером на Next.js можно тут - https://app-dir.vercel.app/error-handling/books). Как правило, дополнительно всегда есть общий для всех дефолтный компонент - страница ошибки, который будет использован в том числе если ошибка произошла вне React (например в любом месте в серверном обработчике запроса).

Какие именно ошибки вообще можно и нужно обрабатывать? Если мы говорим про роуты, вот два основных кейса:

- expected ошибки, связанные с загрузкой данных для роута - когда надо программно выбросить 404 или 500 ошибку и отобразить подходящий UI, это область ответственности meta-фреймворка, ошибку легко поймать
- unexpected ошибки, по сути любой throw внутри React компонента страницы, и такую ошибку достаточно сложно обработать

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

У нас же есть Error Boundary и все ок? - к сожалению нет, он поймает только ошибки в рантайме браузера, если например throw произошел при обработке клика от пользователя.

Остаются ошибки при серверном рендеринге и ошибки гидрации.

Так, мы копнули глубже (https://github.com/reactjs/rfcs/blob/main/text/0215-server-errors-in-react-18.md, и узнали что с 18 версии реакта Suspense тоже обрабатывает ошибки, как раз на сервере и при гидрации.

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

Тут тоже есть нюансы:

- Если используем renderToString на сервере - никак не сможем залогировать ошибку, изменить код ответа, все произойдет “молча”. Только на клиенте, если использовать hydrateRoot, можно залогировать серверную ошибку в onRecoverableError
- С renderToPipeableStream все уже лучше, есть обработчики ошибок, у нас почти полный контроль, но мы все-равно не знаем в каком именно компоненте произошла ошибка (парсить стек-трейс этой ошибки неблагодарное дело), и в fallback UI не имеем прямого доступа к объекту этой ошибки
- Не все пользователи перешли на React 18, мета-фреймворки еще не могут завязаться на этот механизм
- В идеале хочется рендерить один и тот же fallback и в Suspense, и в Error Boundary - но как минимум у Suspense фаллбэка нет доступа к ошибке, уже не получится отрисовать аналогичный UI

По итогу, Suspense в связке с Error Boundary вполне подойдет для повышения надежности и производительности (Selective Hydration) каких-нибудь островков на страницах, таких как микрофронтенды (хотя и тут много чего надо учитывать вне реакта, например падение запроса за JS кодом микрофронта на сервере - надо отрендерить фаллбэк, перезапросить на клиенте, будет много кастомного кода), но реализовать полноценный Page Error Boundary только с этими механизмами не получится.

Так как же устроен этот механизм у Next.js, или у Remix?

Я первый раз исследовал эту тему когда добавлял Page Error Boundary в tramvai в прошлом году - https://tramvai.dev/docs/features/error-boundaries#specific-fallback, и смотрел как раз в исходники Remix, которые порадовали своей простотой и комментариями, в отличие например от некста.
👍10🔥1
Алгоритм работы механизма отображения error UI в Remix не сложный, изящный, с важным минусом который кажется никак не решить снаружи реакта:

- В основе лежит Error Boundary, но скорее его можно назвать Universal Error Boundary - важное отличие в том, что компонент может принимать объект ошибки error снаружи, как пропс. Пример в репе трамвая - https://github.com/Tinkoff/tramvai/blob/main/packages/tramvai/react/src/error/UniversalErrorBoundary.tsx
- Что бы избежать ошибок гидрации из-за рассинхрона разметки, эту error надо сериализовать и передавать на клиент. Пример простых хэлперов что бы перегнать объект ошибки в простой объект и обратно - https://github.com/Tinkoff/tramvai/blob/main/packages/modules/render/src/shared/pageErrorStore.tsx#L11
- Самое главное, и тут как раз скрывается не приятный но простой хак, нам нужно получить эту ошибку. Единственный универсальный способ сделать это - try/catch для renderToString или хэндлеры ошибок у renderToPipeableStream. Но получение этой ошибки означает, что рендер упал - и его придется делать повторно.

При повторном рендере, мы где-то уже храним ошибку, и можем передать ее в пропсы Error Boundary, и не рендерить компонент второй раз.

Важный момент! Мы только предполагаем, что ошибка была где-то в компоненте страницы, но этот механизм никак не гарантирует, что второй рендер тоже не упадет - и тут как раз надо иметь некий Root Error Boundary, который мы бы отдали в ответ на любую неперхваченную ошибку.

Почему только сейчас вспомнил про эту тему - вчера мне стало интересно, что там нового у Next.js 13 и Remix по обработке ошибок, оба перешли на API для стримингового рендеринга реакта, и копаясь в исходниках Remix увидел что много всего изменилось, в том числе по Error Boundaries.

Сначала решил, что нашли какой-то способ, возможно с Suspense, не делать повторный рендер - но нет, докопался до мест, где рендер делается второй и где-то даже и третий раз)

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

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

Очень интересен ваш фидбек, как обрабатываете ошибки, или может быть кто-то глубоко понимает механизмы того-же некста, и сможет раскрыть тему?

Спасибо за внимание!
🔥11👍8
Привет!

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

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

При этом, в нексте уже есть возможность загрузки данных в самих компонентах, благодаря Server Components (серверные компоненты могут быть async, клиентские могут использовать экспериментальный хук use) и подходу render-as-you-fetch.

Это обсудили в твиттере, что делать если нужны данные из одного и того же запроса в компоненте и загрузчике SEO данных.
Решение - экспериментальное cache апи реакта - https://twitter.com/leeerob/status/1619812802453188608?t=W_QqKVZeKSFPTeORs8XBlg&s=19

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

Этот cache под капотом дедуплицирует вызов функции по аргументам.

И для render-as-you-fetch с server components это наверное нормальное решение что бы избежать дубликатов и водопадов запросов, хотя и решаемое на уровне http клиентов (например tinkoff-request https://tinkoff.github.io/tinkoff-request/docs/plugins/cache-deduplicate.html)

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

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

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

Также есть взгляд немного с другой стороны, в tramvai основной механизм загрузки данных - экшены, которых можно сколько угодно добавить для страницы, и они будут выполнены параллельно.
Прямой связи между данными из экшена и компонентом страницы нет, и основным транспортным каналом как раз является встроенный в трамвай стейт-менеджер.
Конечно минусы и тут есть, например это лишний бойлерплейт для простых кейсов, в ряде случаев можно решить использованием react-query вместо стейт-менеджера.
Также встречал сложные кейсы, когда экшены вложены друг в друга многократно (как правило связано со сложностью логики этого кейса)

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

По итогу, не могу сделать какие-то полезные выводы - все делают по разному, и все эти механизмы сделаны не просто так, имеют и плюсы и минусы.

Очень интересно как вы решаете сложные кейсы бизнес-логики в приложениях написанных на мета-фреймворках.
👍11
Привет!

Достаточно давно смотрел на бенчмарк фреймворков от builderIO, и был очень удивлен насколько быстро по ним работает marko.js - https://github.com/BuilderIO/framework-benchmarks#ssr-times

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

Сравнивал marko.js, и remix, затем добавил tramvai в бенчмарк, смотрел все на дефолтном примере с /dashboard, потом на эту же страничку добавил очень много компонентов что бы посмотреть и на тяжелом рендере что происходит.

tramvai и remix - оба делают минимум действий вокруг реакта, можно сказать что это чистый renderToString плюс работа серверного фреймворка (конечно фреймворк-специфичные детали есть, но все не так плохо как у next.js, там свой прикол с post-процессингом HTML перед отправкой клиенту, чем она больше тем хуже - https://github.com/vercel/next.js/issues/35797)

Что интересного по итогу:
На сервере каждая миллисекунда работы важна и повлияет на итоговый RPS и latency нагруженного сервиса.

Пример флеймчарта Remix, на express.js сервере который делает достаточно много лишней работы
tramvai перешел на fastify, и тут все получше, но есть свой оверхэд на инициализацию дерева DI провайдеров под каждый запрос, и в итоге не намного опережает Remix под нагрузками
Так в чем секрет marko.js? На флеймчарте можно увидеть просто минимум работы, и сейчас это коротко разберем.
У marko вообще ряд оптимизаций на уровне компилятора, плюс в документации описано отличие от реакта https://markojs.com/docs/why-is-marko-fast/#high-performance-server-side-rendering:
- react создаем vDOM, затем проходит по нему еще раз и рендерит HTML в строку
- marko рендерит HTML за один проход

И действительно во всех моих тестах marko примерно в 2 и более раза быстрее рендерит HTML, чем react.

При это по RPS marko выигрывает не меньше чем в 3 раза у tramvai и remix - и это вторая часть оптимизации - он делает минимум лишней работы на сервере.

Как я понял из ответа мейнера фреймворка - https://twitter.com/dylan_piercey/status/1561505479347449858?t=dOKFEf405DOPwOrFlqT6Pw&s=19 - они на этапе сборки компилируют нативный node.js сервер (то есть не используют фреймворк) и также в каком-то виде инлайнят роутинг.
И результаты очень крутые!
👍3
Веселый факт - на тяжелой странице, marko.js выдает больше RPS чем Next.js с использованием Static Site Generation!

По сути Next раздает статику медленее чем marko рендерит)

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

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

Также круто, что на рынке фреймворков такая сильная конкуренция, и мы можем выбирать самое лучшее решение под свои особенности, задачи и ресурсы.
25
Попалась новость - Vercel анонсировали Next.js Cache - https://nextjs.org/blog/next-13-2

Также это заанонсил Sebastian Markbage - https://twitter.com/sebmarkbage/status/1628845420121128965?t=Kcjyhz0u9gOePtgm69ubWg&s=19

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

В общем рекламируют такой ISR на стероидах, который отлично совмещается с Server Components.

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

Но вся эта громкая фича - это просто кэширование данных запросов...

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

Никто же до этого не использовал отдельный слой HTTP клиентов с кэшами, все фетчат направо и налево и страдают от медленных ответов.

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

А что вы думаете про анонс, и как далеко зайдет манкипатчинг fetch API?
👍71🔥1🤡1
Вот и SvelteKit начали изящно стримить данные с сервера на клиент - https://svelte.dev/blog/streaming-snapshots-sveltekit

Для сериализации всего что только можно используется либа также от Рича Харриса - devalue

Не так давно (не знаю точно откуда сама идея пошла для мета-фреймворков) такой функционал в Remix добавили, Await + defer - https://remix.run/docs/en/v1/guides/streaming

Как работает фича с React и Remix хорошо и наглядно объяснено в этом gist'е.

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

Во многом это возможно именно из-за поддержи стриминга на уровне фреймворков - это await в темплейтах Svelte и Suspense у React - при использовании стриминговых API для рендеринга React имеет возможность для Suspense с промисами отправить на клиент fallback, а по завершении промиса отправить нужную разметку на клиент для обновления UI

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

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

Очень хочется добавить такую фичу в tramvai, где уже есть свои особенности, для запросов используются Actions, которые имеют небольшой таймаут на выполнение на сервере, и информация об этом передается на клиент.
Если экшен не был выполнен на сервере - он автоматически будет запущен на клиенте, можно назвать это синхронизацией.
И вот тут основной минус, что надо долго ждать запуска клиентского экшена, загрузку всех ресурсов, самой страницы и ее гидрации.
Хорошее поле для улучшений)
👍10
Кстати first-class поддержка промисов в шаблонах не удивлюсь если первыми была у Marko.js, тег await - мейнейнеры фреймворка визионеры)
🔥3
В том же анонсе увидел что у SvelteKit есть универсальные и server-only методы загрузки данных - https://kit.svelte.dev/docs/load#universal-vs-server

Это интересный кейс, что файл с load методом в .server.js файле автоматически становится API роутом, на которое приложение само сходит при SPA-переходе.

Пример запроса для posts страницы - posts/__data.json

И вот для возможности стримить эти JSON данные используется формат NdJSON, в демке видно что даже Content-Type свой - text/sveltekit-data

Понял что звучит очень похоже на то как передаются данные для React Server Components.

Интересная фича, так как более привычные способы загрузки данных чисто на сервере - это делать отдельно API роуты, но может быть я путаю и кто-то из мета-фреймворков уже имеет такие интеграции?
👍2🔥2
Привет!

Интересный кейс с HTTP/2 и preconnect к ресурсам с CDN.

В preconnect входит резолв DNS и установка TLS соединения, на один домен используется как правило один TCP сокет, и этот преконнект по сути переиспользуется для множества запросов за ресурсами.

Какое-то время назад заметил что на tinkoff.ru преконнекты к ресурсам с одного CDN происходят дважды - один сокет для тегов link и изображений, один для шрифтов и JS файлов.

Наконец-то раскопал проблему, для шрифтов и стилей у нас по дефолту проставляется атрибут crossorigin="anonymous", и для них другие правила установки TLS соединения, поэтому браузер выделяет отдельный сокет.

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

Почитать про это подробнее можно по ссылкам:
- https://groups.google.com/a/chromium.org/g/net-dev/c/1tBj5tEoL4g
- https://github.com/w3c/resource-hints/issues/32
- https://crenshaw.dev/preconnect-resource-hint-crossorigin-attribute/

Похоже что в нашем случае имеет смысл делать crossorigin="anonymous" дефолтом и для стилей и для изображений, а вообще в идеале надо раздавать статику с основного домена - тогда отсутствуют расходы на preconnect кроме первого (за HTML) и нет проблем с CORS - кстати это реализовано на avito, ребята большие молодцы.
🔥14
И визуализация проблемы - оба файла расположены на одном CDN
👍6👌2
Привет!

Продолжая тему с preconnect, еще несколько интересных вещей.

Во-первых, не нашел ни одного способа проверить, работает ли добавленный на страницу <link rel="preload" />, кроме сервиса Webpagetest.

Локально его использовать не получится без платного ключа к API, но он без проблем анализирует превью на Codesandbox, вполне удобный способ тестирования.

Сделал демку с ассетами с разных доменов, и добавил на каждый домен preload обычный и c crossorigin (preload будет работать только если совпадает CORS правила для конкретного запроса за ресурсом)

Кстати, почему у нас для каждого скрипта проставлен crossorigin="anonymous" - без этого стектрейс ошибки скрипта с другого домена очень ограничен.
👍1