Frontend-разработка Сравнение фреймворков: React, Vue, Svelte, Angular, Solid — что выбрать
0%

Сравнение фреймворков: React, Vue, Svelte, Angular, Solid — что выбрать

Сравнение фреймворков: React, Vue, Svelte, Angular, Solid — что выбрать

Мы прошли трек почти целиком: от устройства браузера до тестирования. Теперь можно ответить на вопрос, с которого обычно начинают, — «на чём писать?» — и ответить на него не лозунгом, а разбором.

Плохая новость: универсального ответа нет. Хорошая: пространство решений маленькое и хорошо структурировано. Все пять героев этой статьи решают ровно одну задачу — связать состояние с DOM так, чтобы вы не писали element.textContent = ... руками. Различаются они ответом на второй вопрос: как узнать, что именно изменилось. Три возможных ответа порождают три семейства, и почти всё остальное — синтаксис, экосистема, культура — вытекает из этого выбора.

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

Один вопрос: «что изменилось?»

Представьте, что состояние query поменялось на один символ. Где-то в дереве из пятисот узлов должен обновиться один текстовый узел. Фреймворк обязан его найти. Способов ровно три.

1. Пересчитать всё и сравнить (виртуальный DOM). Выполняем тела компонентов заново, получаем новое дерево описаний, сверяем со старым, применяем разницу. Это React и Preact. Модель предельно простая: UI = f(state), вы просто описываете «как должно быть». Расплата — работа пропорциональна размеру перерисованного поддерева, а не размеру изменения, и её приходится сокращать вручную: memo, ключи, стабильные ссылки. Дэн Абрамов подробно разбирает эту модель в «React as a UI Runtime».

2. Запомнить, кто от кого зависит (сигналы, мелкозернистая реактивность). При первом выполнении фиксируем: «этот текстовый узел читает query». При записи в query обновляем ровно подписчиков. Это Solid, Vue 3, Svelte 5, Angular с сигналами. Работа пропорциональна размеру изменения. Расплата — нужно понимать, когда именно читается сигнал: если вы «размотали» его значение слишком рано, связь потеряна, и обновления не будет. Механику графа сигналов мы уже разбирали в статье про управление состоянием.

3. Знать заранее (компиляция). Компилятор видит шаблон целиком, видит, что от query зависит один интерполятор, и генерирует код вида if (dirty & QUERY) text.data = .... Это Svelte 3–4, частично Vue (compiler-informed VDOM), Marko, Vue Vapor. Работа минимальна и известна на этапе сборки. Расплата — магия происходит на сборке: стек-трейсы, отладка и «почему это не реактивно» уезжают в сгенерированный код.

Сколько работы делает каждая модель при изменении одного поля состояния

Именно из этой развилки растут все остальные различия. Модель «пересчитать и сравнить» позволяет писать компоненты как обычные функции и делает возможным конкурентный рендеринг: раз рендер — чистая функция, его можно прервать, выбросить и запустить заново. Модель «граф зависимостей» этого не позволяет (побочные эффекты подписки уже случились), зато не требует мемоизации.

Вот как выглядит путь от клика до пикселя в самой сложной из трёх моделей — React 19 с конкурентным планировщиком:

В Solid этой диаграммы почти нет: обработчик пишет в сигнал, сигнал синхронно (точнее — пакетом в микрозадаче) уведомляет вычисляемые значения и эффекты, один из эффектов пишет в текстовый узел. Ни планировщика, ни фаз, ни сверки. Это одновременно и сила, и ограничение: прервать такое обновление нельзя, поэтому конкурентных возможностей уровня useTransition в сигнальных фреймворках либо нет, либо они реализованы иначе.

Граница между компилятором и рантаймом

Второй структурный вопрос: сколько работы фреймворк выполняет один раз на сборке, а сколько — на каждом устройстве каждого пользователя.

Распределение работы фреймворка между сборкой и браузером

Тренд последних десяти лет однозначен: работа уезжает влево, в компилятор. Vue компилирует шаблон в render-функцию с «patch flags» — метками, которые говорят рантайму, какие атрибуты вообще могут поменяться, так что сверка пропускает статические части (Rendering Mechanism). Svelte пошёл дальше всех и объявил рантайм деталью реализации («Virtual DOM is pure overhead»). Angular всегда компилировал шаблоны AOT-компилятором. React дольше всех держался за «просто JavaScript», но и он пришёл к React Compiler, который анализирует код и расставляет мемоизацию сам.

Важная поправка к картинке: байты фреймворка редко решают. В реальном продукте бандл на 80–90% состоит из вашего кода, UI-кита, локалей, дат, графиков и аналитики. Экономия 30 КБ на фреймворке не спасёт приложение, которое тянет 400 КБ иконок и moment со всеми локалями. Байты решают в двух случаях: встраиваемые виджеты на чужих сайтах и контентные страницы, где JS в принципе почти не нужен.

Одна задача — пять реализаций

Хватит теории. Ниже один и тот же компонент: поле поиска, производный отфильтрованный список, счётчик найденного. Код рабочий и копируемый; типы — TypeScript, потому что во всех пяти экосистемах это давно норма (сам язык разбирает трек TypeScript).

React 19

import { useState, useMemo } from 'react';

type Item = { id: number; title: string };

export function SearchList({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');

  // Производное значение считается на КАЖДОМ рендере компонента.
  // useMemo нужен, только если фильтрация реально дорогая;
  // с включённым React Compiler его обычно можно не писать вовсе.
  const filtered = useMemo(
    () => items.filter((i) => i.title.toLowerCase().includes(query.toLowerCase())),
    [items, query],
  );

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <p>Найдено: {filtered.length}</p>
      <ul>
        {filtered.map((i) => (
          <li key={i.id}>{i.title}</li>
        ))}
      </ul>
    </>
  );
}

Vue 3

<script setup lang="ts">
import { ref, computed } from 'vue'

const props = defineProps<{ items: { id: number; title: string }[] }>()
const query = ref('')

// computed ленив и кэширован: пересчитается только при изменении query или items,
// и только если кто-то его читает.
const filtered = computed(() =>
  props.items.filter((i) => i.title.toLowerCase().includes(query.value.toLowerCase())),
)
</script>

<template>
  <input v-model="query" />
  <p>Найдено: {{ filtered.length }}</p>
  <ul>
    <li v-for="i in filtered" :key="i.id">{{ i.title }}</li>
  </ul>
</template>

Svelte 5

<script lang="ts">
  let { items }: { items: { id: number; title: string }[] } = $props();
  let query = $state('');

  // $derived — тот же ленивый computed, только без .value и без импортов
  const filtered = $derived(
    items.filter((i) => i.title.toLowerCase().includes(query.toLowerCase()))
  );
</script>

<input bind:value={query} />
<p>Найдено: {filtered.length}</p>
<ul>
  {#each filtered as i (i.id)}
    <li>{i.title}</li>
  {/each}
</ul>

Angular

import { Component, ChangeDetectionStrategy, computed, input, signal } from '@angular/core';

@Component({
  selector: 'app-search-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <input [value]="query()" (input)="query.set($any($event.target).value)" />
    <p>Найдено: {{ filtered().length }}</p>
    <ul>
      @for (i of filtered(); track i.id) {
        <li>{{ i.title }}</li>
      }
    </ul>
  `,
})
export class SearchListComponent {
  // signal input: входной проп сам является сигналом
  items = input.required<{ id: number; title: string }[]>();
  query = signal('');

  filtered = computed(() =>
    this.items().filter((i) => i.title.toLowerCase().includes(this.query().toLowerCase())),
  );
}

Обратите внимание на @for/@if — новый управляющий синтаксис шаблонов вместо старых директив *ngFor/*ngIf. И на то, что Angular современный — это standalone-компоненты и сигналы, без NgModule и без Zone.js:

// main.ts — приложение без модулей и без Zone.js
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { App } from './app';

bootstrapApplication(App, {
  providers: [provideZonelessChangeDetection()], // в 18–19 назывался provideExperimentalZonelessChangeDetection
});

Solid

import { createSignal, createMemo, For } from 'solid-js';

export function SearchList(props: { items: { id: number; title: string }[] }) {
  const [query, setQuery] = createSignal('');

  // Тело компонента выполнится РОВНО ОДИН РАЗ.
  // Реактивны не компоненты, а вот эти выражения.
  const filtered = createMemo(() =>
    props.items.filter((i) => i.title.toLowerCase().includes(query().toLowerCase())),
  );

  return (
    <>
      <input value={query()} onInput={(e) => setQuery(e.currentTarget.value)} />
      <p>Найдено: {filtered().length}</p>
      <ul>
        <For each={filtered()}>{(i) => <li>{i.title}</li>}</For>
      </ul>
    </>
  );
}

Пять реализаций отличаются на десяток символов. Это и есть главный практический вывод: синтаксис — не аргумент. Разработчик, знающий одну модель, переучивается на другую за неделю. Различия начинаются там, где абстракция протекает.

Где протекает каждая модель

У каждого фреймворка есть класс ошибок, которые новичок делает неизбежно, а сеньор перестаёт замечать. Это честная цена модели.

React: замыкания и лишние рендеры. Тело компонента — обычная функция, и каждый её вызов создаёт новое замыкание с замороженным снимком состояния.

useEffect(() => {
  // ❌ count заморожен на значении первого рендера — счётчик встанет на 1
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, []);

useEffect(() => {
  // ✅ функциональное обновление не зависит от замыкания
  const id = setInterval(() => setCount((c) => c + 1), 1000);
  return () => clearInterval(id);
}, []);

Второй хронический источник боли — ручная мемоизация. useMemo, useCallback, memo — это не оптимизации «на всякий случай», а костыли к модели «пересчитываем всё». Стабильный useCallback, переданный в немемоизированный компонент, не даёт ничего. React Compiler закрывает большую часть этой темы, и в новом коде правильная стратегия — включить компилятор и не расставлять мемо руками.

Vue: где .value, а где нет. ref в шаблоне разворачивается автоматически, в скрипте — нет. Деструктуризация reactive рвёт связь:

const state = reactive({ count: 0 })
const { count } = state          // ❌ обычное число, реактивность потеряна
const { count: c } = toRefs(state) // ✅ ref, связь сохранена

Плюс историческая раздвоенность: половина статей в интернете написана под Options API и Vue 2, половина — под Composition API и <script setup>. Для новичка это заметный шум, хотя документация Vue — вероятно, лучшая среди всех пяти.

Svelte 5: руны работают не везде. Руны — компиляторные конструкции, а не функции. Они доступны в .svelte и в модулях с расширением .svelte.ts/.svelte.js, и деструктуризация снимает реактивность так же, как в Vue:

<script lang="ts">
  let user = $state({ name: 'Аня' });
  let { name } = user; // ❌ снимок на момент выполнения, обновляться не будет
</script>
// store.svelte.ts — общее состояние без библиотек
export const cart = $state({ items: [] as string[] });
export const total = $derived(cart.items.length);

Отдельная трудность — переход Svelte 4 → 5: $: и сторы из svelte/store работают, но идиомы сменились полностью, и большая часть учебных материалов в сети описывает старый мир (разбор рун от команды).

Angular: две эпохи change detection в одном фреймворке. Классический Angular обходил дерево компонентов после каждого асинхронного события благодаря Zone.js, который патчил setTimeout, промисы и события. OnPush сокращал обход, но требовал иммутабельности:

this.items.push(item);               // ❌ ссылка та же, OnPush-компонент не обновится
this.items = [...this.items, item];  // ✅
this.items.update((v) => [...v, item]); // ✅ и лучше: сигнал

Новый Angular — сигналы плюс zoneless: обход дерева заменяется точечными уведомлениями. Но в реальном проекте вы почти наверняка встретите смесь: сигналы в новых компонентах, RxJS и async-пайпы в старых, NgModule в самых старых. Плюс Angular — единственный из пятёрки, кто требует выучить целый параллельный мир: DI-контейнер, декораторы, RxJS.

Solid: компонент выполняется один раз. Это ломает всю мышечную память React-разработчика. Пропсы — геттеры, поэтому деструктуризация убивает реактивность, а .map по массиву создаёт DOM заново:

function Row(props: { user: User }) {
  const { user } = props;        // ❌ прочитано один раз при создании
  return <span>{props.user.name}</span>; // ✅ чтение внутри JSX реактивно
}

// ❌ пересоздаёт все узлы при любом изменении списка
<div>{items().map((i) => <Row user={i} />)}</div>
// ✅ <For> переиспользует узлы, <Index> — когда меняется содержимое, а не порядок
<For each={items()}>{(i) => <Row user={i} />}</For>

Хорошая новость: этих ловушек конечное число, у Solid они честно перечислены в документации. Плохая — статически они почти не ловятся, только код-ревью и опыт.

Производительность: что показывают бенчмарки

Единственный бенчмарк, на который стоит смотреть, — js-framework-benchmark Стефана Крауса: одинаковая таблица на 1000–10000 строк, одинаковые операции, честные keyed-реализации, публикуемая методика.

Порядок величин, устойчивый из релиза в релиз (геометрическое среднее по операциям, ванильный JS = 1.00):

Фреймворк Отношение к vanilla Память после старта Комментарий
vanillajs 1.00 минимум эталон, писать так нельзя
Solid ~1.05 низкая практический потолок для фреймворка
Svelte 5 ~1.10 низкая сигналы плюс компиляция
Vue 3 ~1.20 средняя VDOM с подсказками компилятора
Angular (signals) ~1.25 средняя zoneless заметно улучшил цифры
React 19 ~1.40–1.50 выше остальных реконсиляция и планировщик стоят денег

Точные числа смотрите на сайте — они меняются с каждой версией. Важнее уметь читать эту таблицу правильно, а читать её обычно не умеют.

Что бенчмарк показывает честно. Стоимость модели обновления на большом списке. Если ваш продукт — таблица на десять тысяч строк с сортировкой, виртуализацией и инлайн-редактированием, разница между 1.05 и 1.45 реальна, вы её увидите на слабом ноутбуке.

Чего он не показывает. Что в обычном приложении рендер фреймворка занимает единицы процентов времени кадра. Остальное — парсинг и исполнение вашего JS, запросы к сети, пересчёт стилей, раскладка, декодирование картинок, сторонние скрипты аналитики и чат-виджета. Разница «в полтора раза» на трёх процентах бюджета — это полтора процента. Тот же LCP улучшается на порядок сильнее одной строчкой fetchpriority="high" на герой-картинке.

Практический критерий: если вы не можете назвать метрику, которая просядет от выбора фреймворка, выбор фреймворка не про производительность.

Реальные бандлы, а не «hello world»

Сравнивать надо не фреймворк, а минимальный рабочий комплект: фреймворк + роутер + состояние + слой данных. Порядок величин, gzip:

Стек Фреймворк Роутер Состояние Данные Итого
React ~45 КБ react-router ~12 КБ zustand ~1 КБ TanStack Query ~13 КБ ~70 КБ
Vue ~34 КБ vue-router ~10 КБ pinia ~2 КБ TanStack Query ~13 КБ ~59 КБ
Angular ~100 КБ встроен сигналы встроены HttpClient встроен ~100 КБ
Svelte ~5–15 КБ SvelteKit руны встроены ~13 КБ или свой ~25 КБ
Solid ~7 КБ @solidjs/router ~5 КБ сторы встроены ~13 КБ или свой ~25 КБ

Angular выглядит тяжёлым, но отдаёт всё из коробки одной командой и одной версией; в React-стеке те же возможности собираются из пяти пакетов с независимыми релизами. Это ровно тот случай, когда «больше байт» может означать «меньше решений и меньше рисков рассинхронизации».

Почему тормозит: диагностика вместо гаданий

Обратите внимание, где на этой схеме находится «модель обновлений фреймворка» — в одном листе из восьми. Это не значит, что она неважна; это значит, что до неё нужно дойти измерением, а не начать с неё.

Чем измерять

Инструменты делятся на общие для платформы и специфичные для фреймворка. Общие важнее.

// Полевые метрики с атрибуцией: сразу видно, где именно теряется INP
import { onINP, onLCP, onCLS } from 'web-vitals/attribution';

onINP(({ value, attribution: a }) => {
  navigator.sendBeacon('/rum', JSON.stringify({
    metric: 'INP',
    value,                          // итог, мс
    target: a.interactionTarget,    // селектор элемента
    inputDelay: a.inputDelay,       // главный поток был занят до обработчика
    processing: a.processingDuration, // работал ваш обработчик и рендер
    presentation: a.presentationDelay, // браузер считал стили и рисовал
  }));
});

Разложение INP на три части сразу отвечает на вопрос «виноват ли фреймворк»: большой inputDelay — это чужой скрипт или гидратация, большой processing — ваш код или рендер, большой presentationDelay — CSS и раскладка. Подробности — в статье про производительность фронтенда.

Дальше — специфика:

  • React — DevTools Profiler, «Highlight updates when components render», флеймграф коммитов. С React Compiler полезен ESLint-плагин, который показывает, какие компоненты компилятор не смог оптимизировать и почему.
  • Vue — Vue DevTools с вкладкой Timeline: видно, какие компоненты перерисовались и по какой зависимости.
  • Angular — Angular DevTools с профайлером change detection: показывает, сколько раз и почему проверялся каждый компонент. На zoneless-приложениях эти цифры резко падают.
  • Svelte / Solid — почти нечего профилировать на уровне фреймворка: обновление точечное. Смотрите обычный Performance-таб DevTools; в Solid есть solid-devtools для просмотра графа сигналов.
  • Все — Long Animation Frames API в Chrome DevTools: показывает длинные кадры вместе с виновным скриптом, работает независимо от фреймворка.

Отдельно — сравнение «на своём проекте». Единственный честный способ выбрать по производительности: собрать один и тот же экран вашего продукта на двух кандидатах и померить.

# Собрать и посмотреть реальный вес, а не «hello world»
npm run build
npx vite-bundle-visualizer            # что именно занимает место
npx size-limit                        # бюджет в CI: падает, если превысили
npx unlighthouse --site http://localhost:4173  # CWV по всем маршрутам разом
// package.json — бюджет, который ломает сборку
{
  "size-limit": [
    { "path": "dist/assets/index-*.js", "limit": "120 kB", "gzip": true },
    { "path": "dist/assets/vendor-*.js", "limit": "90 kB", "gzip": true }
  ]
}

Вы выбираете не фреймворк

Самая частая ошибка в обсуждении — сравнивать React с Vue. На практике вы выбираете не библиотеку рендеринга, а весь комплект вокруг неё.

Мета-фреймворк обычно определяет повседневную жизнь сильнее, чем модель реактивности:

Мета-фреймворк База Сильная сторона Слабая сторона
Next.js React RSC, стриминг, ISR, огромное комьюнити, дефолт индустрии сложность App Router, тесная связка с одним вендором хостинга
Nuxt Vue продуманные соглашения, авто-импорты, модульная экосистема магии больше, чем у Next, отладка «откуда это взялось»
SvelteKit Svelte самый простой роутинг и загрузка данных, отличные адаптеры экосистема тоньше, меньше готовых интеграций
Angular SSR / Analog Angular инкрементальная гидратация, всё официально и поддерживается стартовый вес, меньше вариантов деплоя
SolidStart Solid тонкий и предсказуемый, острова и серверные функции молодой, меньше готовых рецептов
Astro любой UI ноль JS по умолчанию, острова, Server Islands не для приложений с плотным интерактивом

Про сами стратегии — SSR, SSG, ISR, гидратацию и острова — подробно в статье «Роутинг и стратегии рендеринга».

Как мы сюда пришли

Понимать историю полезно: она объясняет, почему у Angular два способа делать всё, а у React — компилятор.

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

Стоимость владения

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

Ритм релизов и обратная совместимость.

  • React — мажоры редкие, миграции щадящие, старый код продолжает работать годами. Цена — накопленная неоднозначность: три способа получить данные, два роутера, устаревшие статьи в поиске.
  • Vue — переход 2 → 3 был болезненным и стоил экосистеме нескольких лет; Vue 2 снят с поддержки в конце 2023 года. Vue 3 с тех пор развивается аккуратно.
  • Angular — мажор каждые полгода по расписанию, 18 месяцев поддержки, ng update автоматически переписывает код схематиками. Это самая предсказуемая политика из всех — если вы готовы обновляться два раза в год.
  • Svelte — переход 4 → 5 сменил идиомы целиком, старый синтаксис поддерживается, но новые материалы пишутся уже под руны.
  • Solid — маленький и стабильный, версия 2.0 в разработке; риск не в поломках, а в размере комьюнити.

Найм и онбординг. Здесь картина устойчива много лет: React — большинство вакансий и резюме, Vue — сильные региональные рынки (Китай, Восточная Европа, часть Азии), Angular — энтерпрайз, банки, госсектор, внутренние системы, Svelte и Solid — единицы вакансий, зато мотивированные кандидаты. Проверяйте цифры сами и в динамике: State of JS, Stack Overflow Developer Survey, npm trends, HTTP Archive Web Almanac и Core Web Vitals Technology Report, где видно распределение реальных метрик по технологиям.

Три вопроса, которые честно оценивают риск:

  1. Если завтра единственный человек, знающий этот стек, уйдёт — за сколько недель мы найдём замену?
  2. Через три года мажорное обновление будет стоить нам недели или квартала?
  3. Есть ли готовые библиотеки для наших скучных задач — таблица с виртуализацией, датапикер с локалями, редактор, графики, загрузка файлов? Отсутствие одной такой библиотеки съедает всю экономию от «более быстрого» фреймворка.

Как выбирать

Сценарии, разобранные по существу:

Задача Разумный выбор Почему
SaaS-дашборд с графиками и таблицами React или Vue готовые таблицы, гриды, чарты; рендер не узкое место
Внутренняя админка банка, 40 разработчиков Angular единый стиль, DI, генераторы, предсказуемые обновления
Лендинг и блог продукта Astro, Hugo, любой SSG JS не нужен, важен LCP и стоимость правок
Виджет чата на чужих сайтах Solid или Preact в Web Component каждый килобайт виден, изоляция обязательна
Мобильное приложение и веб одной командой React с React Native единственный зрелый общий путь
Интерактивная визуализация на 50k элементов Solid или Svelte здесь модель обновлений действительно решает
Постепенное оживление серверного шаблона Vue, Alpine, htmx, Web Components подключается точечно, без переписывания
Проект на два месяца с одним разработчиком тот, который он знает всё остальное неважно

Если решение всё-таки нужно защитить перед командой, оцените кандидатов по весам, а не по ощущениям. Разумный набор критериев с типичными весами: экспертиза команды (×5), наличие библиотек под конкретные задачи продукта (×4), рынок найма (×4), стоимость обновлений и поддержки (×3), качество мета-фреймворка и SSR (×3), производительность рендера (×2), вес рантайма (×1, кроме виджетов). Если после честного заполнения таблицы победитель меняется от перестановки одного веса — значит, выбор не имеет значения, берите знакомое.

Что почти никогда не должно решать: разница в бенчмарках в полтора раза; «меньше строк кода» в примере из README; звёзды на GitHub; личное мнение автора статьи в интернете, включая эту.

Когда фреймворк не нужен вовсе

Полезное упражнение — проверить, нужен ли вам вообще фреймворк. Признаки, что нет: страниц мало, состояние живёт на сервере, интерактив сводится к формам и раскрывающимся блокам, SEO важнее анимаций.

Альтернативы, которые работают в проде:

  • Серверный рендеринг плюс htmx — HTML остаётся источником истины, фрагменты приходят с сервера. Идеально для CRUD-админок, за которыми стоит Django, Rails, Phoenix или ASP.NET.
  • Alpine.js — 15 КБ поведения прямо в атрибутах разметки, когда нужен только тоггл и модалка.
  • Web Components на Lit — когда компоненты должны переживать смену фреймворка: дизайн-система, встраиваемые элементы, долгоживущие внутренние платформы.
  • Просто платформа. <dialog>, popover, View Transitions, :has(), контейнерные запросы, content-visibility закрывают то, ради чего десять лет тянули библиотеки. Про это — статьи по вёрстке и DOM и событиям.

Границы честные: как только появляются оптимистичные обновления, оффлайн, сложные формы с межполевой валидацией и живой совместный доступ — возвращайтесь к фреймворку, ручная синхронизация состояния с DOM обойдётся дороже.

Сосуществование и миграция

Переписывание «с нуля на новом стеке» — самый дорогой и самый часто выбираемый способ. Работающая альтернатива — постепенное замещение (strangler fig), где два фреймворка какое-то время живут на одной странице.

Технический шов между стеками — Web Components: их понимают все фреймворки. Svelte умеет компилировать компонент в кастомный элемент одной строкой:

<!-- RatingWidget.svelte -->
<svelte:options customElement="rating-widget" />

<script lang="ts">
  let { value = 0 }: { value?: number } = $props();
</script>

<div class="stars">{'★'.repeat(value)}{'☆'.repeat(5 - value)}</div>
// Тот же компонент внутри React-приложения
import './RatingWidget.svelte'; // регистрирует <rating-widget>

export function Review({ score }: { score: number }) {
  // атрибуты — строки; сложные данные передавайте через свойства или события
  return <rating-widget value={String(score)} />;
}

Обратный переход — монтирование React-виджета в Vue- или Angular-приложение — делается вручную через createRoot в onMounted/ngAfterViewInit и unmount при уничтожении. Три правила, чтобы это не превратилось в болото:

  1. Один стек владеет роутером и оболочкой. Два роутера на одной странице — гарантированные баги с историей и фокусом.
  2. Общее — не компоненты, а слои ниже UI: дизайн-токены (обычные CSS-переменные), API-клиент, авторизация, аналитика. Их нужно вынести первыми.
  3. Срок жизни переходного состояния фиксируется заранее. Если через год миграция не закончена, вы не мигрируете — вы платите за два фреймворка.

Организационная сторона — микрофронтенды, границы владения, независимый деплой — тема следующей статьи.

Типичные ошибки

  1. Выбор по бенчмарку. Полуторакратная разница в рендере на трёх процентах бюджета кадра не окупает отсутствия нужной библиотеки.
  2. Выбор по хайпу. «Все переходят на X» — обычно наблюдение за конференц-повесткой, а не за продакшеном.
  3. Игнорирование мета-фреймворка. Роутинг, загрузка данных, SSR и деплой определяют повседневную работу сильнее, чем useState против createSignal.
  4. Переписывание вместо рефакторинга. Медленное приложение почти всегда медленно из-за архитектуры данных, лишних запросов и картинок, а не из-за фреймворка. После переписывания те же ошибки воспроизводятся на новом стеке.
  5. Смешение моделей. Сигнальная библиотека поверх React, «свой» реактивный слой поверх Vue — два источника истины и невоспроизводимые баги.
  6. Экзотика без запаса прочности. Редкий фреймворк допустим, если команда готова писать недостающие интеграции сама и это записано в план.
  7. Забытая доступность. Ни один фреймворк не делает интерфейс доступным сам; фокус, роли и объявления придётся продумывать в любом (руководство по доступности).
  8. Оценка «на hello world». Сравнивайте комплекты под вашу задачу и на вашем экране, а не стартовые шаблоны.

Мини-итог

  • Фреймворки различаются ответом на вопрос «что изменилось»: сверка дерева, граф сигналов, компиляция. Всё остальное — следствия.
  • Работа устойчиво уезжает из рантайма в компилятор; к 2026 году к сигналам и компиляции пришли все, кроме React, который выбрал путь компилятора ради конкурентного рендеринга.
  • Разница в бенчмарках реальна на больших списках и почти невидима в обычных продуктах. До вывода «виноват фреймворк» нужно дойти измерением: разложите INP на inputDelay / processing / presentation.
  • Байты фреймворка решают только для встраиваемых виджетов и контентных сайтов; в приложении бандл делает ваш код и библиотеки.
  • Выбирают не библиотеку рендеринга, а комплект: мета-фреймворк, экосистему, найм и цену обновлений через три года.
  • Экспертиза команды — самый весомый критерий из всех. Знакомый стек почти всегда побеждает «технически лучший».
  • Иногда правильный ответ — не брать фреймворк: сервер плюс htmx, Alpine, Web Components или просто платформа.
  • Миграция — постепенное замещение с фиксированным сроком, а не переписывание с нуля.

Источники

Что дальше

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

Архитектура фронтенда: слои, монорепо, микрофронтенды, деплой и мониторинг

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов