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

https://github.com/SuperOleg39

https://twitter.com/ODrapeza

@SuperOleg39
Download Telegram
В чате Reatom увидел очень интересную библиотеку Mitosis - https://github.com/BuilderIO/mitosis

Позволяет писать компоненты на синтаксисе, близком к React и Solid, и компилировать их в нативные компоненты на большинстве фреймворков.

Ещё один потенциальный кандидат на инструмент для создания фреймворк-агностик UI-kit
SuperOleg dev notes pinned «Привет! Несколько дней назад на Github состоялся релиз фреймворка tramvai - https://github.com/TinkoffCreditSystems/tramvai tramvai - это фреймворк для создания SSR приложений на React, внутренняя разработка Тинькофф, и последние полтора года я работаю в…»
Привет!

Несколько дней назад прочитал статью от Shawn Wang https://twitter.com/swyx - "Why do Webdevs keep trying to kill REST?" - https://www.swyx.io/client-server-battle/

В этой статье представлен интересный взгляд на дебаты REST против GraphQL - Swyx рассказывает, что на самом деле эти споры про Smart Servers против Smart Clients (не стал переводить в лоб, возможно тут подойдут термины тонкий / толстые клиент или сервер)

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

Smart Client - это подход, при котором сначала обновляется состояние на клиенте, и затем отправляется на сервер.

Рассмотрим на примерах:

- Кастомный smart client - решение на основе Redux, или сторов Svelte, где мы самостоятельно управляем координацией данных на сервере и клиенте
- Специализированная библиотека для управления состоянием и запросами - React Query, Apollo Client, RxDB, GunDB, WatermelonDB и Absurd-SQL
- Фреймворк, который абстрагирует координацию данных - Next.js или Blitz.js (тут я не совсем понял автора, т.к. Next.js например предоставляет интеграцию с библиотекой swr, аналогом React Query, но в чистом виде ничего специфичного не дает)
- SDK к облачной платформе - Google's Firebase или AWS Amplify / AppSync, из коробки предоставляют интеграции с такими бэкенд ресурсами как авторизация, база данных и другие хранилища

Smart Server - обратный подход, когда обновления состояния сначала отправляются на сервер, который в свою очередь отправляет обновленный view на клиент (HTML чанки, сериализованные React компоненты или XML)

Пример актуальных технологий:

- Phoenix Liveview
- Rails Hotwire
- React Server Components
- ASP.NET Web Forms

Smart Server - это не новый подход, а эволюция традиционного серверного рендеринга.
Laravel, Django, Wordpress и ряд других фреймворков отправляют на клиент отрендеренные HTML шаблоны, а клиент добавляет интерактивность, и отправляет запросы на REST эндпоинты сервера, такое классическое разделение на фронтенд и бэкенд.
Из моего опыта, я успел поучаствовать в двух таких проектах, где php фреймворк отвечал за рендеринг HTML шаблонов.

Почему же мы уходим от старой клиент-серверной парадигмы, и какой подход лучше?
👍1
Про User Experience

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

Smart Clients позволяют делать offline-first приложения с оптимистичными обновлениями, т.е. могут работать без интернета и мгновенно реагировать на любые действия:
- это улучшает интуитивное впечатление пользователя о скорости работы приложения
- но раздувает JS бандлы на клиент, SDK Firebase добавляет 1mb кода в бандл, SDK Amplify около 230kb

Smart Servers напрямую уменьшают количество JS кода, т.к. делают большую часть работы на сервере. Например, при интеграции React Server Components в Facebook, рассказали про уменьшении JS кода на 29% - https://twitter.com/swyx/status/1341151070743982080
- это улучшает скорость первой загрузки сайта, и уменьшает количество JS, загружаемого на клиенте
- но нагрузка по рендерингу каждого кусочка приложения ложится на ваши серверы, и они тратят значительно больше ресурсов

Про Developer Experience

Платформенные SDK - такие платформы как Firebase и AWS Amplify могут предоставлять лучший DX на фронте, т.к. имеют полное понимание вашего бэкенда

Уменьшение бойлерплейта - вместо отдельного написания бэкенд обработчиков, и клиентских запросов к API, мы можем генерировать кастомный API клиент
- Smart Servers радикально уменьшают бойлерплейт, т.к. самостоятельно реализуют механизм синхронизации. В пример приводится LiveView - https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html

Работа offline - платформенные SDK, независимые библиотеки RxDB или Redux Offline, позволяют иметь локальную копию данных, и легко разрешать конфликты обновления этих данных

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

В итоге, оба подхода дают значительные улучшения DX.

Про протоколы

Хороший протокол улучшает UX и DX, например:

Типобезопасность - GraphQL проверяет типы в рантайме, trpc на этапе компиляции

Пропускная способность сети - уменьшение передаваемых данных:
- GraphQL решает проблему запроса избыточных данных (кажется проблема не так важна, если вы не IT гигант)
- Hotwire буквально передает HTML по проводам
- React Server Components отправляют сериализуемые компоненты

Real-time - возможности совместной работы над одним приложением:

- Для этого отлично подходит UDP, WebRTC и WebSockets (отмечу от себя, что WebSockets работает поверх TCP, и это можно отнести к недостаткам протокола, а WebRTC похоже позволяет выбрать транспорт самостоятельно)
- Также подходящими выглядят такие инструменты как Replicache и Croquet
- UDP в целом похож на отличный фундамент для новых инновационных протоколов (HTTP/3 работает поверх UDP)

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

Наш клиентский код должен подключаться к разметке, которую отдал сервер, и добавлять ей интерактивность - это значит, что даже разделив JS код с логикой компонентов, нам придется грузить все эти компоненты на каждой странице, либо вручную реализовывать lazy loading (эту работу мог за нас сделать бандлер и code splitting)

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

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

Но, если посмотреть трезвым взглядом, универсальные SSR приложения на React имеют свои недостатки:

- Мы должны писать наш сервер на NodeJS (знаю, что есть решения для запуска JS кода в других языках, и есть Deno, но кажется все эти решения не оптимальны как минимум с точки зрения производительности) - и это требует экспертизу, которой может не быть у фронтенд разработчиков на текущем проекте.
С другой стороны, есть такие решения как NextJS и tramvai, которые возьмут на себя большую часть сложностей на серверной стороне.

- Это медленно.
Рендеринг и запросы происходят на сервере, что просаживает Time To First Byte.
Из-за ожидания загрузки кода и гидрации на клиенте, просаживается Time To Interactive.
Хорошая статья про виды рендеринга в вэбе от Эдди Османи - https://developers.google.com/web/updates/2019/02/rendering-on-the-web

- Много JavaScript кода (медленно v2).
В первую очередь - это код выбранного фреймворка.
Плюс, даже сложное веб приложение может иметь не так много интерактивных компонентов на странице.
Точечное добавление обработчиков событий и логики обновления DOM вместо отдельного кода на каждый статичный HTML тег позволит сильно уменьшить количество кода на странице.
Определенно, React Server Components частично помогает решить эту проблему, т.к. исключает со стороны клиента лишний код для фетчинга данных.
Для меня очень интересным оказался опыт разработки Github, где, если я правильно понял концепцию, полноценно используется подход Smart Server.

Github разработчики ушли от jQuery, и реализуют интерфейсы с помощью нескольких небольших утилит для делегирования событий, и создания качественных web-components - https://github.blog/2018-09-06-removing-jquery-from-github-frontend/ и https://github.blog/2021-05-04-how-we-use-web-components-at-github/

На бэкенде используется Ruby on Rails, и разработчики "прокачали" работу с шаблонами в Rails, добавив инкапсуляцию в шаблоны с помощью ViewComponent - https://github.blog/2020-12-15-encapsulating-ruby-on-rails-views/

Кстати, посмотреть исходный код фронта Github вы можете с легкостью, т.к. sourcemaps в открытом доступе, и достаточно открыть инструменты разработчика, вкладку Sources, и открыть там например файл https://github.githubassets.com/assets/app/assets/modules/behaviors.ts

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

С точки зрения UX, приведу в пример сравнение нашей главной странички https://tinkoff.ru и страничку репозитория tramvai - https://github.com/TinkoffCreditSystems/tramvai
На tinkoff.ru загружается около 250kb gzip вендор кода и кода приложения, а на github.com - около 200kb.
При этом, страница репозитория выглядит значительно более сложной и интерактивной.

tinkoff.ru разработан на фреймворке tramvai, и каждый блок на странице является микрофронтендом на React.
Мы постоянно работаем над улучшением производительности, но у нас остаются достаточно хардкорные оптимизации, которые могут дать заметный профит - lazy hydration каждого блока на странице, отключение hydration для блоков, которые статические по своей природе, или например tree-shaking DI провайдеров во время компиляции приложения (пример такого подхода у Angular - https://coryrylan.com/blog/tree-shakeable-providers-and-services-in-angular)

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

Возможно ViewComponent - https://github.com/github/view_component - это не совсем Smart Server, а более традиционный подход, но "на стероидах", позволяющий делать более качественные серверные компоненты.
Swyx говорил еще про несколько интересных технологий:

Phoenix LiveView

Phoenix - https://github.com/phoenixframework/phoenix - это серверный фреймворк на Elixir
LiveView - https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html - это расширение фреймворка, реализующую real-time синхронизацию между сервером и клиентом, возможность разделения серверного view кода на компоненты, реализацию реакцие на взаимодействие пользователей на сервере, и позволяющее отказаться от SPA фреймворка на клиенте и в целом писать минимум кода.

Сложно уместить описание в несколько строк, но даже так выглядит мощно!

Rails Hotwire

Hotwire - https://github.com/hotwired/hotwire-rails - расширение для Ruby фреймворка Rails, дает схожие возможности с Phoenix LiveView, одно из отличий - это использование HTML в качестве типа данных общения с сервером, для любых апдейтов DOM.
Подробное видео с разбором работы инструмента можно посмотреть тут - https://hotwired.dev/

Еще одно из отличий Hotwire от LiveView, и кажется преимущество - все частичные апдейты страницы делаются по HTTP протоколу, WebSockets можно использовать для страниц, где нужна совместная работа нескольких пользователей. LiveView же использует WebSockets для всех апдейтов.

Также попалась статья, где еще больше похожих на LiveView и HotWire инструментов - https://dev.to/rajasegar/html-over-the-wire-is-the-future-of-web-development-542c

Для меня это совершенно незнакомый мир, и очень интересно видеть, как по разному может выглядеть фронтенд разработка!
Актуальный и классный доклад от Рича Харриса про подходы к созданию веб-приложений - https://youtu.be/860d8usGC0o

Во многом перекликается с статьей про Smart Servers и Smart Clients от Swyx, на которую я недавно писал обзор - https://t.me/super_oleg_dev/27.

Автор подробно рассматривает преимущества и проблемы традиционного подхода к рендерингу страниц на сервере (MPA приложения) и SPA приложений.
Не забыты и Phoenix LiveView вместе Rails Hotwire, как эволюция MPA подхода, но не решающие всех проблем (работа offline, optimistic updates)

Очень метко про github.com - показал ряд рассинхронизаций интерфейса, бороться с которыми обречено приложение, обновляющее UI по частям, без общего стейта и механизма для рендеринга.

Почти все из проблем из преимуществ уже были озвучены в предыдущих постах - https://t.me/super_oleg_dev/29, поэтому хочу рассказать про то, что Рич предлагает для решения проблем этих подходов.

Автор предлагает новый термин, по аналогии с transitional design - transitional apps, приложения, которые берут лучшее из MPA и SPA миров.

Далее приводит несколько примеров как библиотеки и фреймворки решают недостатки совмещения этих подходов:

- React Server Components (кажется пора сделать отдельный обзор технологии, к сожалению не делал заметок, когда первый раз читал RFC https://github.com/reactjs/rfcs/pull/188)

- marco с их механизмом частичной гидрации интерактивных частей приложения https://github.com/marko-js/marko

- qwic с агрессивным lazy loading всего кода страницы https://github.com/BuilderIO/qwik

- astro с архитектурой islands, также предназначенной для загрузки только необходимого кода https://github.com/snowpackjs/astro

- svelte, который снижает стоимость фреймворка, компилируя только необходимый код, и позволяющий не использовать гидрацию https://github.com/sveltejs/svelte

В конце Рич рассказывает про SvelteKit https://github.com/sveltejs/kit, и как этот инструмент предоставляет лучшее из обоих миров, на примере небольшого демо-приложения.

Если обобщить, SvelteKit из коробки позволяет приложению:

- работать без JS (например для перехода по ссылкам и отправки форм)

- рендерить на этапе сборки статические страницы, с опциональным добавлением JS для интерактивности

- простой деплой всего приложения на комбинацию edge functions и cdn

Классный и короткий доклад, частично анонс будущего доклада Рича Харриса, рекомендую к просмотру!

Отдельно пометил себе попробовать SvelteKit, интересно насколько всё-таки хорошо решается проблема лишнего кода фреймворков.
DX по определению будет отличный благодаря Vite (невероятно крут из коробки на новых приложениях, но есть сомнения, будет ли эффективна разработка приложений где на страницу загружаются сотни и сотни js модулей, даже по http/2)

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

Попалась небольшая статья про загрузку данных на уровне компонентов в NextJS приложениях - https://medium.com/@A__G__B/component-level-data-fetching-in-next-js-with-srr-8d35cdc5849e
Загрузка данных на уровне компонентов обеспечивается с помощью либы https://github.com/kmoskwiak/useSSE

Оказалось, что use-sse использует подход double render (если я не ошибаюсь, его использует Appolo).
Первый рендеринг приложения на сервере позволяет собрать промисы с загрузкой данных для каждого компонента, второй рендеринг выполняется после резолва этих промисов.

Меня всегда сильно смущал такой подход - дело в том, что ReactDOMServer.renderToString и даже ReactDOMServer.renderToNodeStream работают синхронно, и соответственно блокируют event loop.

На гитхабе мне попадались заброшенные попытки форкнуть ReactDOMServer и сделать рендеринг асинхронным, выполнять его по кусочкам, как раз для предотвращения долгой блокировки event loop.

Почему это важно?
React работает быстро на сервере, но для большого дерева компонентов, renderToString может занимать условно 100ms (на примере некоторых наших приложений от 30 до 300 при больших нагрузках).

Скорее всего, это будет самая длинная синхронная операция на вашем сервере.
Фактически, эта операция - самый главный ограничитель вашего RPS на одну ноду с SSR приложением.

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

С такими метриками, при SSR с React можно рассчитывать что одна нода потянет 10 RPS, что достаточно не серьезные нагрузки с точки зрения high load, но пока что это наша фронтовая реальность.

С учётом этих факторов, double rendering кажется чем-то невероятно дорогим.
И в будущем Server Components будут решать проблему загрузки данных на уровне компонентов.

Я очень рассчитываю на React 18 и новую архитектуру работы на сервере.
Новые механизмы для рендеринга на сервере, вместе с Suspense, больше не будут рендерить приложение в один синхронный проход, эта задача будет разделена на отдельные асинхронные задачи (одна задача на один юнит - реакт компонент), аналогично concurrent rendering на клиенте.

Если node.js вместо десятка синхронных задач по 100ms будет получать тысячу задачек по 1ms, это в разы увеличит количество запросов, которые нода может обрабатывать одновременно без существенного влияния перформанс, т.е. новые запросы будут гораздо меньше влиять на время ответа для последующих запросов.

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

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

И возможно в новом прекрасном мире SSR легче будет делать high load :)

Отдельно хочу вспомнить про кэширование.
Конечно, самое эффективное ускорение рендеринга на сервере - это его отсутствие, и в ряде случаев можно кэшировать результаты рендеринга, или даже использовать SSG.

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

В конце хочу порекламировать свой обзор дискуссий reactwg/react-18, в том числе там есть разбор новой архитектуры на сервере - https://superoleg39.notion.site/reactwg-react-18-3914d12cc91e430b974495bffea86472
Привет!

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

Библиотека с 8 лямов скачиваний в неделю, активно используем в tramvai.

Удалить пакеты так просто автор кажется не сможет из-за политики npm после leftpad, поэтому рекомендую проверить зависимости в своих проектах, и по необходимости зафиксировать версию ua-parser-js на 0.7.28

Информации пока немного, завел issue - https://github.com/faisalman/ua-parser-js/issues/536

Надеюсь, что всё-таки скоро откатят пакет, но предупредить будет не лишним)
Мейнтейнер ua-parser-js опубликовал версии пакета без уязвимостей, перекрывающие проблемные по semver - 0.7.30, 0.8.1 и 1.0.1

Это закрывает основные проблемные кейсы - свежая установка диапазонов версий вида ^0.7.28 и latest версии.

Конечно, до отката пакетов в npm остаётся проблема у тех, кто уже успел скачать и закэшировать версии.

Дополнительно, в нашем приватном регистре пакетов мы удалили проблемные версии и запретили их проксирование из публичного npm
Подводя итоги истории с ua-parser-js:

- npm откатили пакет примерно через 6 часов после публикации версии с уязвимостью, это достаточно быстро, но например я успел скачать эту уязвимость около пяти раз (хорошо это было в контейнере)

- мейнтейнер хорошо сделал, что пометил проблемные версии как deprecated, но самым лучшим решением было бы сразу опубликовать нормальные версии, перекрыв по semver проблемные, в итоге это заняло около 4-х часов

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

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

- мейнтейнеру посоветовали включить 2FA

- ряд советов в issue как проверить что вы установили проблемный пакет, как минимум стоит проверить что не запущен процесс jsextension
Привет!

Сегодня стартовала Next.js conf, и вступительное видео посвящено релизу 12 версии Next.js, и планам на развитие инструмента.

Ссылка на это видео, вместе с расписанием и остальными записями - https://nextjs.org/conf/stage/keynote

Ссылка на релиз в блоге Next.js - https://nextjs.org/blog/next-12

Новые возможности Next.js очень круты и особенно вдохновляют меня как референс для развития https://tramvai.dev/!

Во-первых, был официально анонсирован swc в качестве транспилятора, минификатора и парсера CSS для styled-jsx

swc уже достаточно давно проник в кодовую базу некста, также Vercel схантили мейнтейнера swc - https://twitter.com/kdy1dev
По последним релизам видно, что все больше возможностей swc становятся доступны в Next.js по умолчанию, а не за экспериментальными флагами.

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

Оказалось, что это гораздо более глобальная фича.
Новые миддлвары предназначены для деплоя и запуска на edge функциях https://vercel.com/features/edge-functions, и могут выполнять такие функции как аутентификация, A/B тестирование или стриминг ответа от Next.js сервера, и вообще все что придет вам в голову.

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

API этих миддлвар очень похоже на Cloudfire Workers, которые в свою очередь вдохновлялись Service Worker API.

Следующая фича - поддержка react@18 и React Server Components.
Помечена как beta, но примеры работы уже впечатляют.
React Server Components дают возможность делать запросы component-level, и точечно кэшировать результат рендеринга этих компонентов (для сравнения, обычно мы имеем два пути - персонализации нет, можно кэшировать всю страницу, или есть персонализация, и кэшировать практически нечего).

В tramvai планируем начинать интеграцию react@18 после его перехода в beta версию.

Классная фича - поддержка URL Imports (помечена как alpha).
Например, вы можете использовать пакеты без установки и бандлинга из skypack CDN, а в демке https://www.youtube.com/watch?v=_WNeAubn92U импортируется компонент напрямую из Framer, и при изменении в Framer, компонент в приложении обновляется мгновенно.
Впечатляет, правда пока не вижу насколько это широко применимо.

Добавлена опциональная поддержка AVIF - что мы тоже планируем внедрять в tramvai приложения, тесты показывают отличные результаты + появилась поддержка в imgproxy - https://github.com/imgproxy/imgproxy/issues/456

Также во вступительном видео рассказали про Vercel Live - https://vercel.com/live
Мне в первую очередь это интересно с технической стороны.
Этот инструмент использует ES модули для разработки, в качестве dev-сервера самописная разработка, а не Vite или Snowpack.
Мейнтейнеры обещают вынести этот сервер в open source - https://github.com/vercel/next.js/discussions/22406#discussioncomment-884453

Была представлена workflow интеграция Vercel Checks - https://vercel.com/integrations/checkly
Одна из возможностей - собирает Web Vitals метрики на каждый деплой приложения.
Тут у меня основной вопрос, какие погрешности дают такие замеры, кажется точному измерению перформанса приложения с клиентской стороны мешают очень много факторов.
Из интересного - ряд фреймворков с поддеркой SSR добавляет или уже сделал интеграцию с Vite, есть ощущение что Vite и swc начинают доминировать в новом поколении бандлеров и транспайлеров.

При этом, Next.js вряд ли в ближайшие годы будет уходить от использования webpack, т.к. исходный код проекта очень сильно на него завязан.
И похоже даже переход с babel на swc дает такой профит по скорости сборки, что интеграция Vite особенно не рассматривается мейнтейнерами некста - https://github.com/vercel/next.js/discussions/22406#discussioncomment-1300572
Привет!

Последний месяц занимаюсь несколькими интересными задачами для фреймворка tramvai.

Важная на мой взгляд техническая задача - поддержка автоматической загрузки страниц из файловой системы, и генерация роутов приложения на основе этих страниц.
Такой подход вы можете увидеть у Next.js, Gatsby.js, Umi.js, Remix и ряда других SSR фреймворков.

Этот подход во-первых сильно упрощает добавление новых страниц в приложение, во-вторых из коробки позволяет делать эффективное разделение кода.

В tramvai исторически сложилось, что существует объект Bundle для группировки страниц и их общей логики - https://tramvai.dev/en/docs/concepts/bundle
Для code splitting на уровне страниц, необходима ручная работа - оборачивание этих страниц в утилиту lazy.

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

Изначально хотел использовать подход Next.js, и генерировать отдельные webpack entry points на каждую страницу, но это оказалось излишним, и гораздо проще генерировать код страниц, обернутых в lazy (использует @loadable/component под капотом), из которых автоматически будут сгенерированы отдельные чанки.

По итогам разработки, фича оказалась не очень сложной технически, и при этом очень хорошо вписалась в фреймворк.
Актуальную документацию можно посмотреть тут - https://tramvai.dev/en/docs/features/file-system-pages/
Два небольших example приложения тут - https://github.com/TinkoffCreditSystems/tramvai/tree/main/examples

Кстати, есть отличная статья как file system роутинг делается в несколько строк кода с помощью https://vitejs.dev/guide/features.html#glob-import !
К сожалению, не смогу найти ссылку на саму статью(
Другая задача - разработка рекомендаций и практических советов по организации структуры и архитектуры в tramvai приложениях.

Нашей команде кажется очень перспективным подход Feature Sliced - https://feature-sliced.design/

В рамках исследования подготовил example приложение tramvai + feature sliced - https://github.com/SuperOleg39/tramvai-feature-sliced

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

Активное использование модулей в этом example вдохновлено Angular, как и Dependency Injection подход в tramvai в целом)

Дополнительно написал RFC для внутреннего обсуждения, с мотивацией и рядом советов по структуре, плюс обзор и пример интеграции feature sliced.
Публичная версия доступна в Notion - https://superoleg39.notion.site/RFC-Feature-Sliced-691a287028ff457fb1759f5e30d1da85

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

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

Ссылки на статьи:

- https://blog.plasmic.app/posts/why-remix-is-worth-your-attention/
- https://marbiano.dev/into-remix/on-rails

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

Не так давно, я добавлял документацию и example для небольшой фичи tramvai - экспорт статических HTML страниц - https://tramvai.dev/en/docs/features/static-html-export

Сначала я не сильно различал этот функционал и полноценный SSG, но в процессе ознакомления с концепциями и реализациями в различных фреймворках, и как мне показалось, сформировал полную картинку.

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

Статическая генерация страниц - тоже работает на этапе сборки, но это более гибкое решение, т.к. сгенерированные страницы продолжает отдавать само приложение. Таким образом, мы можем совмещать SSG и SSR для разных страниц, и поддерживать такие фичи как i18n.

Затем я узнал про инкрементальную генерацию страниц, концептуально очень крутая фича от NextJS - https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration
Incremental Static Regeneration (ISR) позволяет генерировать страницы в рантайме, по требованию, и приложение кэширует эти страницы после рендеринга на небольшой промежуток времени.
Это отлично подходит для приложений с огромным количеством страниц, например интернет-магазины, которые вряд ли могут себе позволить сборку и релиз приложения при каждом изменении в БД.
Короткий кэш должен заметно снижать нагрузку на приложение, и при этом не будет большой проблемы с устаревшими данными на страницах.

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

Вернемся к Remix. Минус ли это, что фреймворк не поддерживает SSG и ISR?
Заодно я посмотрел документацию SvelteKit - есть аналог SSG https://github.com/sveltejs/sapper/issues/1324#issuecomment-800638474, но нет ISR https://github.com/sveltejs/kit/issues/661
В последнем issue автор Svelte, Рич Харрис, также говорит про возможность кэширования результатов SSR на клиенте и CDN.

И на самом деле, это очень крутая концепция!
У нас есть такие заголовки как Cache-Control, браузеры и CDN отлично их поддерживают, и эти заголовки достаточно гибкие.
И нет необходимости каких-то сложный уникальных реализация для SSG/ISR у разных фреймворков, т.к. этот функционал можно просто делегировать тем, кто уже хорошо в него умеет.

Из нюансов, кэширование на стороне CDN требует наличия этого CDN между пользователем и приложением, что кажется не привычно для SSR приложений.
Это одна из причин, по которым Remix нацелен на запуск at the edge (например в Cloudflare Workers)

Кстати, в документации к SvelteKit увидел классную идею, что можно кэшировать даже персонализованые странички в браузере через Cache-Control: private - https://kit.svelte.dev/docs#loading-output-maxage
Конечно эффективность такого кэша требует исследований, т.к. его время жизни по определению должно быть очень небольшим.

Хорошо и коротко разные виды рендеринга также описаны в доке SvelteKit - https://kit.svelte.dev/docs#appendix, с плюсами и недостатками решений.

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

Мысли про ветвление в коде, code coverage и всего такого.

Сегодня, при обновлении rollup, получил ошибку сборки пакета, в хуке кастомного плагина отстутствовал контекст this.
Кейс использования контекста в хуке описан в документации, ничего экзотического, можно сказать это core функционал https://rollupjs.org/guide/en/#thisparse

Покопался в исходниках, благодаря стектрейсу быстро нашел проблему, и свежий PR, в котором добавили ошибку, написал автору (https://github.com/rollup/rollup/commit/7c4728377fce727338b835d0dba3425319ec5626#r64105152), получил очень быстрый ответ от мейнтейнера rollup, думаю фикс тоже прилетит оперативно - тут вообще никаких проблем, рядовой случай, просто очень наглядный пример для темы, про которую хочу поразмышлять.

Почему я вообще пришел к этой теме.
Пока разбирался с проблемой, вспомнил что почти год назад заносил PR с исправлением мелкого бага https://github.com/rollup/rollup/pull/4340, в том же самом файле.
Код из этого файла подключается только при наличии экспериментального флага https://rollupjs.org/guide/en/#perf, и оборачивает все плагины и их хуки, для вычисления таймингов выполнения.
Получается, код может очень сильно поменять важный функционал библиотеки, API плагинов, и создает полноценное ветвление для всей логики этого API.
Такое ветвление в идеале требует создания дополнительного дублирующего набора тестов, что может сильно усложнить их поддержку.

У rollup очень высокий code coverage в среднем, 98%, но низкий у проблемного файла, меньше 60%.
Тестами не покрыт как раз код, оборачивающий плагины, и как написал в комментарии к моему репорту мейнтейнер бандлера Lukas Taegert-Atkinson, для начала достаточно базового теста с минимальной проверкой plugin API.
Не уверен что этого будет достаточно для предотвращения всех ошибок при использовании плагинов и флага perf, т.к. coverage отчет не умеет учитывать взаимосвязь разных параметров, когда требуется проверить покрытие одной и той же строки кода в разных условиях, но в любом случае, наглядный пример, что code coverage может подсветить проблемные места в коде, и что ветвление в коде требует тщательного тестирования.

В библиотеках очень неплохо работает coverage, но даже у самых простых пакетов может быть такое ветвление, как различные окружения.
Это конечно не ветвление именно в коде, но я не знаю более подходящего названия.
Например, для проверки работоспособности пакета в разных версиях NodeJS, в Github Actions можно использовать матрицу с тестовыми джобами https://docs.github.com/en/github-ae@latest/actions/using-jobs/using-a-build-matrix-for-your-jobs.
Хорошая идея, но как мне кажется непозволительно дорогая по времени, если требуется прогонять огромное количество тестов за одну джобу.

Еще один пример разного окружения - это SSR приложения.
На примере tramvai, в модулях фреймворка содержится большое количество интеграционных тестов.
В большинстве случаев, приходится тестировать один и тот же функционал на сервере, и в браузере, особенно это касается роутинга.
Дополнительное ветвление в роутинге появляется из-за того, что необходимо тестировать SPA переходы, и обычные переходы между страницами (tramvai предоставляет два разных модуля под оба кейса https://tramvai.dev/docs/features/routing#base-modules).
В целом, если вы пишете библиотеки для SSR приложений, в коде не избежать ветвления вроде проверок на typeof window, или на уровне точек входа в пакеты, мы даже добавили в документацию рецепты по разделению кода на различных уровнях https://tramvai.dev/en/docs/guides/universal/.

Еще в кодовой базе tramvai достаточно много ветвления с проверкой условия process.env.NODE_ENV, и что особенно важно, часть этих проверок зависит от production окружения. Кажется, что такие места особенно важно покрывать тестовыми сценариями.
По поводу code coverage и SSR - к сожалению, coverage не работает для интеграционных тестов. Вот небольшой комментарий по поводу coverage в репозитории NextJS https://github.com/vercel/next.js/discussions/11512#discussioncomment-2486.
Теоретически, можно собрать отчет с серверного кода из Jest, отчет с клиентского кода из Puppeteer, смержить их в один общий отчет, я даже когда-то заводил задачку на это в наш таск-трекер, но теперь совсем не уверен в успехе этой идее, и в потенциальной пользе от таких отчетов.

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

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

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

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

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

В общем, пишите хорошие тесты, простой код, думайте о пользователях, и не расстраивайтесь из-за багов в используемых вами продуктах!
👍8🎉1
Привет!

Прочел сегодня статью Resumable JavaScript with Qwik - https://dev.to/this-is-learning/resumable-javascript-with-qwik-2i29, и пропал на несколько часов в различных статьях / тредах / демках, посвященных Qwik и серверному рендерингу с гидрацией в целом.

Начну издалека.

В своих постах я несколько раз касался темы различных видов рендеринга web приложений, и концепции, лежащие в основе Qwik, кажутся полноценной эволюцией существующих подходов.

Давайте вспомним основные варианты рендеринга, существующие на данный момент (частично рассмотрены в статье Rendering on the web - https://developers.google.com/web/updates/2019/02/rendering-on-the-web)

- Статический HTML - ручное создание страниц приложения
- CSR - клиентский рендеринг приложения, стандартные SPA приложения
- SSG - генерация страниц на этапе сборки, либо в рантайме с кэшированием
- Динамический серверный рендеринг - рендеринг страниц на стороне серверного фреймворка с использованием шаблонизатора (классический подход с php, python, ruby)
- SSR - рендеринг страниц на сервере и гидрация на клиенте с использованием универсального кода приложения (метафреймворки)
- Smart Servers - эволюция подхода “динамический серверный рендеринг”, которая нацелена на перенос всей возможной логики на сторону сервера (Phoenix LiveView, Rails Hotwire, React Server Components)
- Прогрессивный SSR - потоковый (streaming) рендеринг на стороне сервера и/или частичная гидрация приложения (Marko.js, Astro.js + Islands architecture, React 18)

С точки зрения конечного пользователя приложения, эти подходы отличаются тем, как быстро ответит сервер, как быстро будет показан контент, и как скоро этот контент станет интерактивным. Первые два пункта отражает Web Vitals метрика LCP, а за интерактивность отвечает FID.

Если я не ошибаюсь, самое сильное влияние на FID оказывает количество JavaScript кода, загружаемого и исполняемого на странице. При использовании SSR подхода, требуется гидрация на клиенте, что подразумевает загрузку и исполнение всего кода текущей страницы, и различного вспомогательного кода, даже при качественном code splitting.

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

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

Подход Smart Servers очень хорош в плане уменьшения клиентского кода, так как любое обновления состояние, и рендеринг новой разметки, происходят на сервере. К минусам подхода могу отнести повышение нагрузок на сервера, непривычные для frontend разработчиков технологии и очень высокие требования к сетевому соединению на клиенте.

Прогрессивная и частичная гидрация (partial hydration) - подходы, направленные на загрузку кода для гидрации по требованию, и гидрацию на уровне отдельных компонентов страницы.

Такой подход с некоторыми ограничениями можно реализовать даже в приложениях с React 17 версии и ниже, используя легальный хак с dangerouslySetInnerHTML - https://github.com/Tinkoff/tramvai/blob/main/packages/libs/lazy-render/src/lazy-render.tsx. Пример можно увидеть на www.tinkoff.ru, код для гидрации футера будет загружен только при попадании футера в область видимости.

Partial hydration кажется панацеей, но на самом деле полноценная реализация этого подхода имеет несколько трудностей. Например, гидрация в большинстве фреймворков направлена сверху вниз. Соответственно, даже если нам надо “оживить” одну кнопку глубоко в дереве компонентов странички, для гидрации требуется весь код родительских компонентов.

Подробно эти проблемы, и основные подходы к оптимизации гидрации, рассмотрены в статье Why Efficient Hydration in JavaScript Frameworks is so Challenging - https://dev.to/this-is-learning/why-efficient-hydration-in-javascript-frameworks-is-so-challenging-1ca3

И тут мы наконец-то подходим к Qwik и к статье, с которой начался этот пост.
👍3