Сравнение фреймворков: 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 с конкурентным планировщиком:
работа поставлена в очередь с приоритетом S->>C: render phase — выполнить тела компонентов C-->>S: новое дерево элементов Note over S: сверка со старым деревом;
фазу можно прервать и начать заново S->>D: commit phase — применить патч, синхронно D-->>B: пересчёт стилей, раскладка, отрисовка S->>C: useLayoutEffect (до отрисовки), затем useEffect
В 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-стеке те же возможности собираются из пяти пакетов с независимыми релизами. Это ровно тот случай, когда «больше байт» может означать «меньше решений и меньше рисков рассинхронизации».
Почему тормозит: диагностика вместо гаданий
а не фреймворка: смотрите SSR/SSG"] C -- нет --> E{"LCP-ресурс найден поздно?"} E -- да --> F["preload, fetchpriority,
убрать ленивую загрузку с героя"] E -- нет --> G["Смотрите TTFB: бэкенд, CDN, кэш"] B -- "долго откликается на клики" --> H{"INP: где основная задержка?"} H -- "input delay" --> I["Главный поток занят:
гидратация, сторонние скрипты, длинные задачи"] H -- "processing" --> J{"Профайлер: что в стеке?"} J -- "рендер фреймворка" --> K["Вот здесь модель обновлений
наконец имеет значение"] J -- "ваш код" --> L["Алгоритм, лишние вычисления,
синхронный localStorage, JSON.parse"] H -- "presentation" --> M["Раскладка и отрисовка:
тяжёлый CSS, большие слои, отсутствие content-visibility"] B -- "дёргается при скролле" --> N["Layout thrashing, анимация не-composited свойств"]
Обратите внимание, где на этой схеме находится «модель обновлений фреймворка» — в одном листе из восьми. Это не значит, что она неважна; это значит, что до неё нужно дойти измерением, а не начать с неё.
Чем измерять
Инструменты делятся на общие для платформы и специфичные для фреймворка. Общие важнее.
// Полевые метрики с атрибуцией: сразу видно, где именно теряется 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, где видно распределение реальных метрик по технологиям.
Три вопроса, которые честно оценивают риск:
- Если завтра единственный человек, знающий этот стек, уйдёт — за сколько недель мы найдём замену?
- Через три года мажорное обновление будет стоить нам недели или квартала?
- Есть ли готовые библиотеки для наших скучных задач — таблица с виртуализацией, датапикер с локалями, редактор, графики, загрузка файлов? Отсутствие одной такой библиотеки съедает всю экономию от «более быстрого» фреймворка.
Как выбирать
блог, лендинг, документация, магазин"} B -- да --> C["Astro или SSG.
SPA под контентный сайт — инженерная ошибка"] B -- нет --> D{"Это виджет,
встраиваемый на чужие сайты?"} D -- да --> E["Solid, Svelte или Preact
плюс упаковка в Web Component"] D -- нет --> F{"Крупная организация,
десятки команд, горизонт 5+ лет?"} F -- да --> G["Angular: единообразие, DI,
расписание релизов, ng update"] F -- нет --> H{"Уже есть команда,
знающая конкретный стек?"} H -- да --> I["Берите его.
Знание команды весит больше бенчмарков"] H -- нет --> J{"Нужен React Native,
RSC или редкая библиотека?"} J -- да --> K["React и Next.js"] J -- нет --> L{"Команда маленькая,
ценит скорость разработки?"} L -- да --> M["Vue и Nuxt либо Svelte и SvelteKit"] L -- нет --> K
Сценарии, разобранные по существу:
| Задача | Разумный выбор | Почему |
|---|---|---|
| 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 при уничтожении. Три правила, чтобы это не превратилось в болото:
- Один стек владеет роутером и оболочкой. Два роутера на одной странице — гарантированные баги с историей и фокусом.
- Общее — не компоненты, а слои ниже UI: дизайн-токены (обычные CSS-переменные), API-клиент, авторизация, аналитика. Их нужно вынести первыми.
- Срок жизни переходного состояния фиксируется заранее. Если через год миграция не закончена, вы не мигрируете — вы платите за два фреймворка.
Организационная сторона — микрофронтенды, границы владения, независимый деплой — тема следующей статьи.
Типичные ошибки
- Выбор по бенчмарку. Полуторакратная разница в рендере на трёх процентах бюджета кадра не окупает отсутствия нужной библиотеки.
- Выбор по хайпу. «Все переходят на X» — обычно наблюдение за конференц-повесткой, а не за продакшеном.
- Игнорирование мета-фреймворка. Роутинг, загрузка данных, SSR и деплой определяют повседневную работу сильнее, чем
useStateпротивcreateSignal. - Переписывание вместо рефакторинга. Медленное приложение почти всегда медленно из-за архитектуры данных, лишних запросов и картинок, а не из-за фреймворка. После переписывания те же ошибки воспроизводятся на новом стеке.
- Смешение моделей. Сигнальная библиотека поверх React, «свой» реактивный слой поверх Vue — два источника истины и невоспроизводимые баги.
- Экзотика без запаса прочности. Редкий фреймворк допустим, если команда готова писать недостающие интеграции сама и это записано в план.
- Забытая доступность. Ни один фреймворк не делает интерфейс доступным сам; фокус, роли и объявления придётся продумывать в любом (руководство по доступности).
- Оценка «на hello world». Сравнивайте комплекты под вашу задачу и на вашем экране, а не стартовые шаблоны.
Мини-итог
- Фреймворки различаются ответом на вопрос «что изменилось»: сверка дерева, граф сигналов, компиляция. Всё остальное — следствия.
- Работа устойчиво уезжает из рантайма в компилятор; к 2026 году к сигналам и компиляции пришли все, кроме React, который выбрал путь компилятора ради конкурентного рендеринга.
- Разница в бенчмарках реальна на больших списках и почти невидима в обычных продуктах. До вывода «виноват фреймворк» нужно дойти измерением: разложите INP на
inputDelay/processing/presentation. - Байты фреймворка решают только для встраиваемых виджетов и контентных сайтов; в приложении бандл делает ваш код и библиотеки.
- Выбирают не библиотеку рендеринга, а комплект: мета-фреймворк, экосистему, найм и цену обновлений через три года.
- Экспертиза команды — самый весомый критерий из всех. Знакомый стек почти всегда побеждает «технически лучший».
- Иногда правильный ответ — не брать фреймворк: сервер плюс htmx, Alpine, Web Components или просто платформа.
- Миграция — постепенное замещение с фиксированным сроком, а не переписывание с нуля.
Источники
- js-framework-benchmark — единственное сравнение с воспроизводимой методикой (исходники и правила)
- Dan Abramov. React as a UI Runtime; React 19 и React Compiler
- Rich Harris. Virtual DOM is pure overhead; Introducing runes
- Vue: Rendering Mechanism и Reactivity in Depth
- Angular: Signals, Zoneless, Release schedule и LTS
- Solid: Intro to reactivity и сравнение с другими фреймворками
- Ryan Carniato. Components are Pure Overhead
- Astro: Islands architecture
- Core Web Vitals Technology Report — реальные метрики по фреймворкам на живых сайтах
- Web Almanac и State of JS — адоптация и настроения, читать вместе, а не по отдельности
- web.dev: Aurora — как команда Chrome работает над производительностью фреймворков
Что дальше
Фреймворк выбран — и почти сразу выясняется, что он отвечает лишь за небольшую часть вопросов. Как разложить код по слоям, чтобы бизнес-логика не растворилась в компонентах; когда монорепо, а когда отдельные репозитории; нужны ли микрофронтенды и какую цену они берут; как всё это собирать, деплоить и наблюдать в проде.
Архитектура фронтенда: слои, монорепо, микрофронтенды, деплой и мониторинг