Пока индустрия разработки ПО увлечена large language models (LLMs), экосистема фронтенда тихо раскололась на три конкурирующие, но взаимосвязанные архитектурные парадигмы. Между доминированием реактивных фреймворков, простотой hypermedia в духе true REST и децентрализованной устойчивостью «SQL везде» разработчики выбирают уже не просто библиотеку, а место, где живут данные: на сервере, на клиенте или и там и там.
Три конкурирующие архитектуры, если совсем грубо
Веб-разработчикам давно знакомы React и целая галактика похожих реактивных фреймворков вроде Angular, Vue и Svelte. Почти десятилетие они задавали тон благодаря конкуренции и взаимному влиянию. HTMX и приложения на основе hypermedia отстаивают возврат к настоящему RESTful thin client, а рядом существуют Hotwire и Unpoly.
Можно сказать, что reactivity и hypermedia — это два противоположных лагеря. Где-то между ними находится local-first SQL, предлагающий размещать SQL прямо в браузере. Картина немного размыта, потому что local SQL может работать и рядом с React.
По-прежнему безопасно говорить, что реактивный фреймворк в связке с JSON API на бэкенде и хранилищем данных (SQL или другим) остается de facto стандартом. Но эта монолитная модель уже начинает распадаться на части — и в этом есть кое-что очень интересное.
Где лежит вес данных
Данные, конечно, — центральная масса веб-приложений. То, где они хранятся и как перемещаются, создает гравитацию, вокруг которой вращается все остальное. Каждая из этих архитектур предлагает свой способ обращаться с этой гравитацией, со своими преимуществами и компромиссами.
Hypermedia (например, HTMX): по возможности держать данные вне клиента. Клиент — это всего лишь визуальное представление серверных данных. Бэкенд-«API» отвечает за формирование markup, основанной на данных. Для сервера API можно использовать любое хранилище данных.
React и аналоги: на клиенте работает сложный stateful engine, а разработчик синхронизирует это состояние с бэкендом через RESTful JSON API calls. Сервер на бэкенде обычно остается «глупым» и в основном вызывает другие сервисы, чтобы обеспечить business logic или data persistence.
Local-first SQL: данные распределяются по клиентам, как в React и подобных решениях, но совсем по-другому. Хотя данные автоматически синхронизируются напрямую с хранилищем данных, например Postgres, API-сервер на бэкенде используется только для специализированных сервисных вызовов — не для data persistence.
Итого:
- HTMX: data gravity находится на сервере.
- React: data gravity распределена между сервером и клиентом.
- Local-first: data gravity находится на клиенте.
Сравнение подходов
Помимо технических характеристик, developer experience у этих парадигм заметно различается. Но при всех различиях они пересекаются в нескольких интересных местах. Давайте посмотрим внимательнее.
React и аналоги
Reactivity — это мир, в котором мы работаем уже 15 лет. У нас есть целая вселенная фреймворков: React, Angular, Vue, Svelte, Solid и full-stack варианты вроде Next.js, Nuxt, SvelteKit, Astro и так далее. Красота этих решений — в базовой reactive idea. У вас есть state, состоящее из переменных, а UI обновляется автоматически. UI — это чистая функция состояния: $UI = f(state).
Недостаток в том, что поверх этого постепенно, почти незаметно нарастает интенсивная сложность. Сначала она кажется побочной, но на самом деле это прямой результат исходной предпосылки: построить state engine в браузере.
Итог такой: у вас есть два состояния — браузер и база данных. Reactive engine становится слоем согласования. Добавьте сюда присущую сложность управления browser state, и получится довольно много для фронтенд-разработчиков, которым все это нужно держать в голове.
Чтобы справиться с такой сложностью, выжать больше производительности и улучшить developer experience, вокруг React выросла разветвленная империя инструментов и техник. Даже только для React есть React Server Components, сложные библиотеки управления состоянием вроде Redux или Zustand, а также orchestration layers вроде TanStack Query для ручной инвалидации кэша.
На бэкенде мы работаем с JSON APIs (или GraphQL), которые могут разрастаться до неудобного уровня как еще один слой boilerplate, но при этом выигрывают почти универсальным пониманием.
HTMX и похожие решения (Hotwire, Unpoly)
HTMX — это как HTML, у которого появились суперспособности. С его помощью можно делать огромную часть того, для чего обычно берут реактивные фреймворки, включая весь AJAX, большую часть partial rendering и эффекты, всего лишь несколькими аккуратно добавленными атрибутами.
Значительное время вы проводите на сервере, используя template engine вроде Pug, Thymeleaf или Kotlin DSL. Именно там вы объединяете данные из persistence service с markup. Сгенерированная разметка включает атрибуты HTMX.
Обычно шаблоны декомпозируют, то есть разбивают на отдельные фрагменты. Идея в том, чтобы иметь фрагмент, который можно использовать внутри более крупного UI для построения всего layout, а при необходимости — использовать отдельно, если он нужен в ответе на AJAX-запрос.
Hypermedia с HTMX — очень мощная модель. По сути, вы действительно используете REST, то есть передаете representational state.
Hotwire и Unpoly — похожие библиотеки. В случае Hotwire можно добиться заметной функциональности и производительности даже без изменения HTML, просто используя Turbo Frames, чтобы перехватывать клики по ссылкам и отправку форм, автоматически превращая стандартную навигацию по страницам в частичные обновления DOM.
Красота hypermedia-подходов в том, что вы получаете много, отдавая мало. Вы остаетесь как можно ближе к HTML — этому образцу простоты. С другой стороны, вы отказываетесь от части той самой изощренной мощи, которую дают реактивные фреймворки.
Local-first apps
Local-first development — новичок на сцене. Как и React с друзьями, local-first хранит данные в двух местах, но делает это радикально иначе. В своей наиболее базовой форме это означает запуск базы данных в браузере, синхронизируемой с удаленным хранилищем через syncing engine. Подобные вещи уже были в NoSQL-базах вроде CouchDB или через IndexedDB API, но современный браузер поднимает это на новый уровень с Wasm-based database engine, например SQLite.
Пользователь получает небольшой срез полного набора данных — это называют partial replication или bucket, также «shape». Front-end app взаимодействует с этими данными напрямую, а инфраструктура автоматически занимается синхронизацией. Большое преимущество здесь — сильная поддержка offline, потому что на устройстве клиента реально находится база данных.
Это серьезный отход от цикла request-response. В local-first вы не запрашиваете данные, а подписываетесь на них. Сеть превращается в фоновый daemon, который согласует локальное и удаленное состояние с помощью CRDTs (conflict-free replicated data types). CRDTs гарантируют, что если два пользователя правят задачу офлайн, слияние произойдет безболезненно, а не хаотично.
Использование SQL везде тоже упрощает картину, хотя это частично компенсируется непривычной и довольно сложной архитектурной схемой. Нужен syncing engine вроде PowerSync или Electric SQL, и для него нужно поддерживать набор правил. Кроме того, нужно настроить auth и взаимодействие между базой данных и syncing engine.
Local-first убирает и API server, и HTML template server. Он переносит весь слой согласования данных в автоматизированный syncing engine, работающий по правилам, заданным разработчиком.
Интересно, что local-first SQL можно использовать как data driver для React (и других reactive engines) или для обычного vanilla HTML + JS. Поэтому это любопытный альтернативный взгляд на архитектуру веба, который не зависит от конкретного фронтенда.
Пожалуй, самое странное сочетание, о котором стоит задуматься, — это совместное использование HTMX и local-first SQL. Это похоже на архитектуру безумного ученого, а значит, разработчики, конечно, уже это делают. В такой схеме back-end HTMX template engine фактически оказывается service worker, который запускает SQL engine. Теоретически вы получаете простоту HTMX и сверхскорость плюс offline-функциональность local SQL.
Reactivity, hypermedia или local-first? Как выбирать
Мы все еще живем в эпоху, когда выбор по умолчанию — React плюс JSON API. Оттуда можно экспериментировать с более новаторскими фреймворками вроде Svelte или Solid. Если вам нужен изящный способ использовать RESTful simplicity, стоит попробовать HTMX или Hotwire. Local-first SQL — экзотическое решение, подходящее сейчас для таких систем, как Linear или Notion, но пока довольно смелое для большинства обычных production-задач.
Шире говоря, появление этой трилеммы означает конец идеи «единственно верного пути» в веб-разработке. Мы уходим от library wars к миру архитектурного выбора.
Выбор между reactivity, hypermedia и local-first — это не только выбор кода. Это выбор того, где вы хотите разместить данные.
- Если вы хотите, чтобы данные были серверным документом, выбирайте hypermedia.
- Если вы хотите, чтобы данные были shared memory state, выбирайте reactivity.
- Если вы хотите, чтобы данные были распределенной базой данных, выбирайте local-first.
И, конечно, эти подходы можно сочетать, стремясь получить для проекта нужный набор преимуществ.
По мере того как монолит JSON-over-the-wire продолжает дробиться, лучшими архитекторами станут не те, кто знает больше всего hooks или атрибутов. Ими станут те, кто понимает вес своих данных и выбирает архитектуру, позволяющую данным двигаться наиболее свободно. Войны фреймворков закончились, но борьба за сеть только начинается.
Материал — перевод статьи с английского.
Оригинал: The front-end architecture trilemma: Reactivity vs. hypermedia vs. local-first apps
