Перейти к содержимому

29. Svelte vs React vs Vue

Привет! 👋 Выбор фреймворка — это одно из самых спорных решений в мире фронтенда. У каждого свои фанаты, у каждого свои недостатки. Давай разберём это честно, без фанатизма, с конкретными примерами кода.

Представь три ресторана: React — это McDonald’s (везде есть, всегда предсказуемо, огромное меню), Vue — это уютное кафе (приятная атмосфера, доступно), а Svelte — это модный foodtruck с авторской кухней (маленький, но потрясающий). Зависит от того, что ты ищешь!


ХарактеристикаSvelte 5React 19Vue 3
ПодходКомпиляторRuntime (VDOM)Runtime (VDOM)
Bundle size (app)~10-30 KB~130-200 KB~50-80 KB
Performance⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Кривая обученияПологаяКрутаяСредняя
TypeScript DX⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
РеактивностьСигналыХукиOptions/Composition
SSR фреймворкSvelteKitNext.jsNuxt.js
Экосистема⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Работа на рынке⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
BoilerplateМинимальныйСреднийМинимальный
ТестированиеVitest + PWJest + RTLVitest + VTU
СообществоРастущееОгромноеБольшое
МобайлSvelteNativeReact NativeCapacitor/Ionic
Веб-компоненты✅ ВстроеноЧерез библиотекиВстроено

Одна задача — три решения: счётчик с derived значением

<!-- Svelte 5 -->
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
function increment() {
count++;
}
</script>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onclick={increment}>+1</button>
// React 18+
import { useState, useMemo } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const doubled = useMemo(() => count * 2, [count]);
return (
<>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</>
);
}
<!-- Vue 3 Composition API -->
<script setup lang="ts">
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
</script>
<template>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">+1</button>
</template>

<!-- Svelte 5 — минимальный boilerplate -->
<script lang="ts">
let email = $state('');
let password = $state('');
let emailError = $derived(
!email ? '' :
!email.includes('@') ? 'Неверный email' : ''
);
let passwordError = $derived(
!password ? '' :
password.length < 8 ? 'Минимум 8 символов' : ''
);
let isValid = $derived(!emailError && !passwordError && !!email && !!password);
async function handleSubmit() {
if (!isValid) return;
await login(email, password);
}
</script>
<form onsubmit={handleSubmit}>
<input bind:value={email} type="email" placeholder="Email" />
{#if emailError}<span>{emailError}</span>{/if}
<input bind:value={password} type="password" placeholder="Password" />
{#if passwordError}<span>{passwordError}</span>{/if}
<button type="submit" disabled={!isValid}>Войти</button>
</form>
// React — больше кода, хуки
import { useState, useMemo, FormEvent } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [touched, setTouched] = useState({ email: false, password: false });
const emailError = useMemo(() => {
if (!email) return '';
if (!email.includes('@')) return 'Неверный email';
return '';
}, [email]);
const passwordError = useMemo(() => {
if (!password) return '';
if (password.length < 8) return 'Минимум 8 символов';
return '';
}, [password]);
const isValid = !emailError && !passwordError && !!email && !!password;
async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!isValid) return;
await login(email, password);
}
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={() => setTouched(t => ({ ...t, email: true }))}
type="email"
placeholder="Email"
/>
{touched.email && emailError && <span>{emailError}</span>}
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
onBlur={() => setTouched(t => ({ ...t, password: true }))}
type="password"
placeholder="Password"
/>
{touched.password && passwordError && <span>{passwordError}</span>}
<button type="submit" disabled={!isValid}>Войти</button>
</form>
);
}

stores.ts
<!-- Svelte: stores или reactive classes -->
<script lang="ts">
import { writable, derived } from 'svelte/store';
export const user = writable<User | null>(null);
export const cart = writable<CartItem[]>([]);
export const cartTotal = derived(cart, $cart =>
$cart.reduce((sum, item) => sum + item.price * item.qty, 0)
);
</script>
<!-- В компоненте: -->
<script lang="ts">
import { cart, cartTotal } from '$lib/stores';
import { user } from '$lib/stores';
</script>
<p>{$user?.name}</p>
<p>Итого: {$cartTotal} ₽</p>
<button onclick={() => cart.update(c => [...c, newItem])}>
Добавить
</button>
store.ts
// React: обычно Zustand или Redux Toolkit
import { create } from 'zustand';
interface CartStore {
cart: CartItem[];
user: User | null;
addItem: (item: CartItem) => void;
setUser: (user: User | null) => void;
total: () => number;
}
export const useStore = create<CartStore>((set, get) => ({
cart: [],
user: null,
addItem: (item) => set((s) => ({ cart: [...s.cart, item] })),
setUser: (user) => set({ user }),
total: () => get().cart.reduce((sum, item) => sum + item.price * item.qty, 0),
}));
// В компоненте:
function CartTotal() {
const total = useStore((s) => s.total());
return <p>Итого: {total} ₽</p>;
}

<!-- Svelte 5 -->
<script lang="ts">
// onMount = $effect (первый запуск после монтирования)
$effect(() => {
const ws = new WebSocket('wss://example.com');
ws.onmessage = handleMessage;
return () => ws.close(); // cleanup = return функция
});
// $effect.pre = beforeUpdate
$effect.pre(() => {
// до обновления DOM
});
</script>
// React
import { useEffect, useLayoutEffect, useRef } from 'react';
function Component() {
const wsRef = useRef<WebSocket>();
// Аналог $effect:
useEffect(() => {
const ws = new WebSocket('wss://example.com');
ws.onmessage = handleMessage;
wsRef.current = ws;
return () => ws.close(); // cleanup
}, []); // [] = только при монтировании
// Аналог $effect.pre:
useLayoutEffect(() => {
// до DOM paint
}, []);
return null;
}

SvelteKit:
src/routes/+page.svelte → /
src/routes/blog/[slug]/+page.svelte → /blog/:slug
+page.ts / +page.server.ts → Load функции
Next.js App Router:
app/page.tsx → /
app/blog/[slug]/page.tsx → /blog/:slug
React Server Components → Данные в компоненте
Nuxt 3:
pages/index.vue → /
pages/blog/[slug].vue → /blog/:slug
useFetch() / useAsyncData() → Данные

💼 Управление состоянием формы: сравнение

Заголовок раздела «💼 Управление состоянием формы: сравнение»

bind: vs onChange: тащиться или нет?

<!-- Svelte — двустороннее связывание встроено -->
<input bind:value={name} />
<!-- Эквивалентно: value={name} oninput={(e) => name = e.target.value} -->
<!-- С трансформацией: -->
<input
value={name.toUpperCase()}
oninput={(e) => name = e.currentTarget.value.toLowerCase()}
/>
// React — всегда явный onChange (controlled component)
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
<!-- Vue — v-model как в Svelte -->
<input v-model="name" />
<!-- Эквивалентно: :value="name" @input="name = $event.target.value" -->

✅ Команда уже знает React
✅ Нужны React Native мобильные приложения
✅ Важен размер рынка труда
✅ Нужны специфические React библиотеки (Framer Motion, React Query)
✅ Большое enterprise приложение с 50+ разработчиками
✅ Нужна максимально документированная экосистема
✅ Нравится Options API (близко к HTML)
✅ Команда знает Vue / Angular
✅ Нужно плавно мигрировать с Angular
✅ Проект в Азии (Vue очень популярен там)
✅ Нужен Nuxt.js с его экосистемой
✅ Новый проект без legacy ограничений
✅ Производительность критически важна
✅ Команда хочет изучить что-то новое
✅ Небольшой или средний проект
✅ Cloudflare Workers или edge computing
✅ Нужен маленький bundle (медленный интернет, мобайл)
✅ Хочешь кайфовать от разработки

React → Svelte:

// Постепенная миграция возможна:
// 1. Создай новые фичи в Svelte
// 2. Используй Svelte компоненты как Web Components в React
// 3. Переписывай компонент за компонентом
// Svelte как Web Component:
// <svelte:options customElement="my-button" />
// Потом в React: <my-button onClick={...} />

Vue → Svelte:

Vue Options API → Svelte 4 (похожий синтаксис)
Vue Composition API → Svelte 5 ($state/$derived/$effect)
Vuex/Pinia → Svelte Stores / Reactive classes
Nuxt → SvelteKit
Vue Router → SvelteKit routing

GitHub Stars (2024):
React 220k+ (Meta backed)
Vue 207k+ (Community driven)
Svelte 77k+ (Vercel backed)
npm downloads в неделю:
React ~25M
Vue ~4M
Svelte ~800K
Job listings:
React ~70%
Vue ~15%
Svelte ~5%
Developer satisfaction:
Svelte ⭐⭐⭐⭐⭐ (State of JS 2023: #1)
Vue ⭐⭐⭐⭐
React ⭐⭐⭐⭐