Przejdź do treści
Frontend

Svelte i SvelteKit - Nowa era tworzenia aplikacji frontendowych

Opublikowano:
·6 min czytania·Autor: MDS Software Solutions Group

Svelte SvelteKit Nowa

frontend

Svelte i SvelteKit - Nowa era tworzenia aplikacji frontendowych

Świat frontendu nie stoi w miejscu. Po latach dominacji React i Vue na scenę wkracza Svelte - framework, który fundamentalnie zmienia sposób myślenia o budowaniu interfejsów użytkownika. Zamiast działać w przeglądarce jak tradycyjne frameworki, Svelte przenosi ciężar pracy na etap kompilacji, generując zoptymalizowany, imperatywny kod JavaScript. W połączeniu ze SvelteKit - pełnoprawnym meta-frameworkiem do budowy aplikacji - otrzymujemy narzędzie, które łączy wyjątkową wydajność z niespotykaną prostotą kodu.

Kompilator zamiast Virtual DOM#

Tradycyjne frameworki jak React czy Vue wykorzystują Virtual DOM - abstrakcyjną reprezentację drzewa DOM w pamięci. Przy każdej zmianie stanu framework porównuje nowy Virtual DOM ze starym (diffing), a następnie aplikuje minimalne zmiany do prawdziwego DOM. Choć to podejście działa dobrze, generuje narzut wydajnościowy i wymaga dostarczenia kodu frameworka do przeglądarki.

Svelte odrzuca ten paradygmat. Zamiast interpretować komponenty w runtime, Svelte jest kompilatorem, który podczas budowania aplikacji przekształca deklaratywny kod komponentów w zoptymalizowany JavaScript. Ten wygenerowany kod bezpośrednio manipuluje DOM-em, bez żadnej warstwy pośredniej.

<!-- Svelte kompiluje to do natywnych operacji DOM -->
<script>
  let count = $state(0);

  function increment() {
    count++;
  }
</script>

<button onclick={increment}>
  Kliknięto {count} razy
</button>

Powyższy komponent Svelte zostanie skompilowany do kodu, który bezpośrednio wywołuje element.textContent = ... i element.addEventListener(...). Nie ma tu żadnego Virtual DOM, żadnego diffingu - jedynie precyzyjne, chirurgiczne aktualizacje DOM.

Korzyści podejścia kompilacyjnego#

  • Mniejsze bundle'e - przeglądarka nie musi pobierać kodu frameworka (brak runtime)
  • Szybsze renderowanie - brak narzutu związanego z Virtual DOM i reconciliation
  • Niższe zużycie pamięci - nie trzeba utrzymywać drzewa Virtual DOM w pamięci
  • Lepsza wydajność na słabszych urządzeniach - mniej obliczeń w runtime

System reaktywności w Svelte 5 - Runes#

Svelte 5 wprowadził rewolucyjny system reaktywności oparty na Runes - specjalnych sygnałach kompilatorowych, które rozpoczynają się od znaku $. Runes zastąpiły wcześniejszy, oparty na przypisaniach system reaktywności, oferując większą precyzję, lepszą kompozycję i pełną kompatybilność z TypeScript.

$state - reaktywny stan#

Rune $state deklaruje reaktywną zmienną stanu. Każda zmiana tej zmiennej automatycznie aktualizuje wszystkie miejsca w DOM, które od niej zależą.

<script>
  let name = $state('Svelte');
  let items = $state([1, 2, 3]);

  // Obiekty i tablice są głęboko reaktywne
  function addItem() {
    items.push(items.length + 1); // automatycznie wykryte!
  }
</script>

<h1>Witaj, {name}!</h1>
<p>Elementy: {items.join(', ')}</p>
<button onclick={addItem}>Dodaj element</button>

$derived - wartości pochodne#

Rune $derived tworzy wartość obliczaną na podstawie innych stanów reaktywnych. Svelte automatycznie śledzi zależności i przelicza wartość tylko wtedy, gdy zależności się zmienią.

<script>
  let width = $state(10);
  let height = $state(20);

  // Automatycznie przeliczane gdy width lub height się zmieni
  let area = $derived(width * height);

  // Dla bardziej złożonych obliczeń
  let description = $derived.by(() => {
    if (area > 100) return 'Duży prostokąt';
    if (area > 50) return 'Średni prostokąt';
    return 'Mały prostokąt';
  });
</script>

<p>Powierzchnia: {area} ({description})</p>

$effect - efekty uboczne#

Rune $effect pozwala reagować na zmiany stanu i wykonywać efekty uboczne, takie jak logowanie, synchronizacja z zewnętrznymi API czy manipulacja DOM.

<script>
  let query = $state('');
  let results = $state([]);

  $effect(() => {
    // Automatycznie śledzi zależność od `query`
    if (query.length < 3) {
      results = [];
      return;
    }

    const controller = new AbortController();

    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(r => r.json())
      .then(data => { results = data; });

    // Funkcja czyszcząca
    return () => controller.abort();
  });
</script>

<input bind:value={query} placeholder="Szukaj..." />
{#each results as result}
  <p>{result.title}</p>
{/each}

$props - właściwości komponentów#

Rune $props definiuje właściwości przyjmowane przez komponent, z pełnym wsparciem dla destrukturyzacji i wartości domyślnych.

<!-- Button.svelte -->
<script>
  let {
    variant = 'primary',
    size = 'md',
    disabled = false,
    children,
    onclick,
    ...rest
  } = $props();
</script>

<button
  class="btn btn-{variant} btn-{size}"
  {disabled}
  {onclick}
  {...rest}
>
  {@render children()}
</button>

Anatomia komponentu Svelte#

Komponenty Svelte to pliki .svelte zawierające trzy sekcje: logikę (<script>), szablon HTML i style (<style>). Style są domyślnie scoped - dotyczą wyłącznie danego komponentu.

<script lang="ts">
  import type { User } from '$lib/types';

  let { user }: { user: User } = $props();
  let isExpanded = $state(false);

  let initials = $derived(
    user.firstName[0] + user.lastName[0]
  );
</script>

<div class="user-card" class:expanded={isExpanded}>
  <div class="avatar">{initials}</div>
  <div class="info">
    <h3>{user.firstName} {user.lastName}</h3>
    <p>{user.email}</p>
  </div>
  <button onclick={() => isExpanded = !isExpanded}>
    {isExpanded ? 'Zwiń' : 'Rozwiń'}
  </button>

  {#if isExpanded}
    <div class="details">
      <p>Dołączył: {user.joinDate.toLocaleDateString('pl-PL')}</p>
      <p>Rola: {user.role}</p>
    </div>
  {/if}
</div>

<style>
  .user-card {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    padding: 1rem;
    border: 1px solid #e2e8f0;
    border-radius: 0.5rem;
    transition: box-shadow 0.2s;
  }

  .user-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }

  .avatar {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    background: #6366f1;
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: bold;
  }

  .expanded {
    border-color: #6366f1;
  }
</style>

Stores - globalny stan aplikacji#

Svelte oferuje wbudowany system zarządzania globalnym stanem za pomocą stores. Stores to reaktywne kontenery danych, które mogą być współdzielone między komponentami.

// src/lib/stores/cart.ts
import { writable, derived } from 'svelte/store';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

export const cartItems = writable<CartItem[]>([]);

export const cartTotal = derived(cartItems, ($items) =>
  $items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

export const cartCount = derived(cartItems, ($items) =>
  $items.reduce((sum, item) => sum + item.quantity, 0)
);

export function addToCart(product: { id: string; name: string; price: number }) {
  cartItems.update(items => {
    const existing = items.find(i => i.id === product.id);
    if (existing) {
      existing.quantity++;
      return [...items];
    }
    return [...items, { ...product, quantity: 1 }];
  });
}

export function removeFromCart(id: string) {
  cartItems.update(items => items.filter(i => i.id !== id));
}

Użycie w komponencie ze składnią autosubscribe ($):

<script>
  import { cartItems, cartTotal, cartCount, removeFromCart } from '$lib/stores/cart';
</script>

<h2>Koszyk ({$cartCount} produktów)</h2>

{#each $cartItems as item (item.id)}
  <div class="cart-item">
    <span>{item.name} x{item.quantity}</span>
    <span>{(item.price * item.quantity).toFixed(2)} zł</span>
    <button onclick={() => removeFromCart(item.id)}>Usuń</button>
  </div>
{/each}

<p class="total">Suma: {$cartTotal.toFixed(2)} zł</p>

SvelteKit - pełny meta-framework#

SvelteKit to oficjalny meta-framework dla Svelte, analogiczny do Next.js dla React czy Nuxt dla Vue. Oferuje wszystko, czego potrzebujesz do budowy nowoczesnych aplikacji web: routing oparty na systemie plików, renderowanie po stronie serwera, generowanie statyczne i wiele więcej.

Routing oparty na plikach#

SvelteKit wykorzystuje system routingu oparty na strukturze katalogów w folderze src/routes. Każdy katalog z plikiem +page.svelte tworzy nową trasę.

src/routes/
├── +page.svelte          → /
├── +layout.svelte        → Layout dla całej aplikacji
├── about/
│   └── +page.svelte      → /about
├── blog/
│   ├── +page.svelte      → /blog
│   ├── +page.server.ts   → Load function dla /blog
│   └── [slug]/
│       ├── +page.svelte  → /blog/:slug
│       └── +page.server.ts
├── api/
│   └── products/
│       └── +server.ts    → API endpoint: /api/products
└── (auth)/
    ├── +layout.svelte    → Layout grupowy (bez wpływu na URL)
    ├── login/
    │   └── +page.svelte  → /login
    └── register/
        └── +page.svelte  → /register

Layouts - współdzielone szablony#

Layouts w SvelteKit pozwalają definiować wspólną strukturę strony (nagłówek, nawigacja, stopka), która jest współdzielona przez podstrony.

<!-- src/routes/+layout.svelte -->
<script>
  import Header from '$lib/components/Header.svelte';
  import Footer from '$lib/components/Footer.svelte';
  let { children } = $props();
</script>

<div class="app">
  <Header />
  <main>
    {@render children()}
  </main>
  <Footer />
</div>

<style>
  .app {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  }
  main {
    flex: 1;
    padding: 2rem;
    max-width: 1200px;
    margin: 0 auto;
  }
</style>

Load functions - ładowanie danych#

Funkcje load to serce mechanizmu pobierania danych w SvelteKit. Mogą działać zarówno na serwerze (+page.server.ts), jak i uniwersalnie (+page.ts).

// src/routes/blog/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';

export const load: PageServerLoad = async ({ url, locals }) => {
  const page = Number(url.searchParams.get('page')) || 1;
  const limit = 10;

  const posts = await db.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
    skip: (page - 1) * limit,
    take: limit,
    select: {
      slug: true,
      title: true,
      excerpt: true,
      createdAt: true,
      author: { select: { name: true } }
    }
  });

  const total = await db.post.count({ where: { published: true } });

  return {
    posts,
    pagination: {
      page,
      totalPages: Math.ceil(total / limit),
      total
    }
  };
};

Dane zwrócone przez load są automatycznie dostępne w komponencie strony:

<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';
  import PostCard from '$lib/components/PostCard.svelte';

  let { data }: { data: PageData } = $props();
</script>

<h1>Blog</h1>

<div class="posts-grid">
  {#each data.posts as post (post.slug)}
    <PostCard {post} />
  {/each}
</div>

{#if data.pagination.totalPages > 1}
  <nav class="pagination">
    {#each Array(data.pagination.totalPages) as _, i}
      <a
        href="/blog?page={i + 1}"
        class:active={data.pagination.page === i + 1}
      >
        {i + 1}
      </a>
    {/each}
  </nav>
{/if}

Strategie renderowania: SSR, SSG i CSR#

SvelteKit oferuje elastyczny wybór strategii renderowania na poziomie poszczególnych tras.

// SSR (domyślnie) - renderowanie na serwerze przy każdym żądaniu
export const ssr = true;

// SSG - statyczne generowanie w czasie budowania
export const prerender = true;

// CSR - renderowanie wyłącznie po stronie klienta
export const ssr = false;
export const csr = true;

// Hybrydowe - statyczna strona z hydracją klienta
export const prerender = true;
export const csr = true;

Form Actions - obsługa formularzy#

Form Actions to elegancki mechanizm SvelteKit do obsługi formularzy po stronie serwera, działający bez JavaScript (progressive enhancement).

// src/routes/contact/+page.server.ts
import type { Actions } from './$types';
import { fail } from '@sveltejs/kit';
import { z } from 'zod';

const contactSchema = z.object({
  name: z.string().min(2, 'Imię musi mieć co najmniej 2 znaki'),
  email: z.string().email('Nieprawidłowy adres email'),
  message: z.string().min(10, 'Wiadomość musi mieć co najmniej 10 znaków')
});

export const actions: Actions = {
  default: async ({ request }) => {
    const formData = await request.formData();

    const data = {
      name: formData.get('name') as string,
      email: formData.get('email') as string,
      message: formData.get('message') as string
    };

    const result = contactSchema.safeParse(data);

    if (!result.success) {
      return fail(400, {
        data,
        errors: result.error.flatten().fieldErrors
      });
    }

    await sendEmail(result.data);

    return { success: true };
  }
};
<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { ActionData } from './$types';

  let { form }: { form: ActionData } = $props();
</script>

<form method="POST" use:enhance>
  <label>
    Imię
    <input name="name" value={form?.data?.name ?? ''} />
    {#if form?.errors?.name}
      <span class="error">{form.errors.name[0]}</span>
    {/if}
  </label>

  <label>
    Email
    <input name="email" type="email" value={form?.data?.email ?? ''} />
    {#if form?.errors?.email}
      <span class="error">{form.errors.email[0]}</span>
    {/if}
  </label>

  <label>
    Wiadomość
    <textarea name="message">{form?.data?.message ?? ''}</textarea>
    {#if form?.errors?.message}
      <span class="error">{form.errors.message[0]}</span>
    {/if}
  </label>

  <button type="submit">Wyślij</button>

  {#if form?.success}
    <p class="success">Wiadomość została wysłana!</p>
  {/if}
</form>

API Routes#

SvelteKit pozwala tworzyć endpointy API za pomocą plików +server.ts:

// src/routes/api/products/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '$lib/server/database';

export const GET: RequestHandler = async ({ url }) => {
  const category = url.searchParams.get('category');
  const page = Number(url.searchParams.get('page')) || 1;

  const products = await db.product.findMany({
    where: category ? { category } : undefined,
    skip: (page - 1) * 20,
    take: 20
  });

  return json(products);
};

export const POST: RequestHandler = async ({ request, locals }) => {
  if (!locals.user?.isAdmin) {
    throw error(403, 'Brak uprawnień');
  }

  const body = await request.json();
  const product = await db.product.create({ data: body });

  return json(product, { status: 201 });
};

Przejścia i animacje#

Jedną z najbardziej wyróżniających cech Svelte jest wbudowany system przejść i animacji. Zamiast sięgać po zewnętrzne biblioteki, możesz elegancko animować elementy za pomocą dyrektyw.

<script>
  import { fade, fly, slide, scale } from 'svelte/transition';
  import { flip } from 'svelte/animate';
  import { quintOut } from 'svelte/easing';

  let items = $state([
    { id: 1, text: 'Pierwszy' },
    { id: 2, text: 'Drugi' },
    { id: 3, text: 'Trzeci' }
  ]);

  let showPanel = $state(false);

  function addItem() {
    items = [...items, {
      id: Date.now(),
      text: `Element ${items.length + 1}`
    }];
  }

  function removeItem(id: number) {
    items = items.filter(i => i.id !== id);
  }
</script>

<button onclick={() => showPanel = !showPanel}>
  Pokaż panel
</button>

{#if showPanel}
  <div transition:fly={{ y: -20, duration: 300 }}>
    <p>Panel z animacją fly</p>
  </div>
{/if}

<button onclick={addItem}>Dodaj element</button>

<ul>
  {#each items as item (item.id)}
    <li
      animate:flip={{ duration: 300 }}
      in:scale={{ duration: 200, easing: quintOut }}
      out:fade={{ duration: 150 }}
    >
      {item.text}
      <button onclick={() => removeItem(item.id)}>×</button>
    </li>
  {/each}
</ul>

Wsparcie dla TypeScript#

Svelte i SvelteKit oferują doskonałe wsparcie dla TypeScript. Typy generowane automatycznie przez SvelteKit zapewniają bezpieczeństwo typów dla load functions, form actions i parametrów tras.

<script lang="ts">
  import type { PageData } from './$types';

  interface Product {
    id: string;
    name: string;
    price: number;
    inStock: boolean;
  }

  let { data }: { data: PageData } = $props();

  let searchQuery = $state('');
  let sortBy = $state<'name' | 'price'>('name');

  let filteredProducts = $derived(
    data.products
      .filter((p: Product) =>
        p.name.toLowerCase().includes(searchQuery.toLowerCase())
      )
      .sort((a: Product, b: Product) =>
        sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)
      )
  );
</script>

Svelte vs React vs Vue - porównanie#

Rozmiar bundle'a#

| Framework | Minimalny bundle (gzip) | |-----------|------------------------| | Svelte 5 | ~2 KB | | Vue 3 | ~33 KB | | React 18 | ~42 KB |

Svelte generuje znacząco mniejsze bundle'e, ponieważ kod frameworka jest wkompilowany - przeglądarka pobiera tylko to, co jest faktycznie używane.

Developer Experience#

| Cecha | Svelte | React | Vue | |------------------------|-----------------|------------------|------------------| | Krzywa uczenia się | Łagodna | Średnia | Łagodna | | Boilerplate | Minimalny | Znaczny (hooks) | Umiarkowany | | Wbudowane animacje | Tak | Nie | Tak (ograniczone)| | Scoped CSS | Domyślnie | CSS Modules/CSS-in-JS | Scoped atrybut| | Wiązanie dwukierunkowe | bind:value | Manualne | v-model | | Zarządzanie stanem | Runes + Stores | useState/Redux | ref/Pinia |

Wydajność runtime#

Svelte regularnie zajmuje czołowe pozycje w benchmarkach wydajności frontendowych frameworków. Brak narzutu Virtual DOM i kompilacja do imperatywnego kodu przekłada się na:

  • Szybsze aktualizacje DOM
  • Niższe zużycie pamięci
  • Lepszą responsywność na słabszych urządzeniach
  • Szybszy Time to Interactive (TTI)

Kiedy wybrać Svelte i SvelteKit?#

Svelte sprawdzi się doskonale w następujących scenariuszach:

  • Aplikacje wymagające najwyższej wydajności - sklepy internetowe, dashboardy, aplikacje real-time
  • Projekty z ograniczonym budżetem na transfer danych - aplikacje mobilne, rynki rozwijające się
  • Strony z intensywnymi animacjami - wbudowany system przejść eliminuje potrzebę zewnętrznych bibliotek
  • MVP i prototypy - minimalna ilość boilerplate'u przyspiesza development
  • Projekty statyczne z elementami interaktywnymi - SSG w SvelteKit w połączeniu z wyspami interaktywności
  • Aplikacje full-stack - SvelteKit z form actions i API routes eliminuje potrzebę oddzielnego backendu

Kiedy rozważyć alternatywy?#

  • Ogromny ekosystem bibliotek UI jest kluczowy - React ma największy wybór gotowych komponentów
  • Zespół ma bogate doświadczenie z React/Vue - koszt przeszkolenia może być czynnikiem
  • Wymagana jest integracja z React Native - Svelte nie ma odpowiednika dla aplikacji mobilnych

Podsumowanie#

Svelte i SvelteKit reprezentują nową filozofię budowania aplikacji frontendowych. Podejście kompilacyjne, intuicyjny system reaktywności oparty na Runes, wbudowane animacje i elegancki routing SvelteKit tworzą ekosystem, który jest jednocześnie potężny i prosty w użyciu.

Rosnąca popularność Svelte, coraz większa baza komponentów i narzędzi, a także wsparcie ze strony Vercel (które zatrudniło twórcę Svelte - Richa Harrisa) świadczą o tym, że Svelte to nie chwilowy trend, lecz technologia, która na stałe zagościła w krajobrazie nowoczesnego frontendu.

Potrzebujesz nowoczesnej aplikacji frontendowej?#

W MDS Software Solutions Group tworzymy wydajne aplikacje webowe z wykorzystaniem najnowocześniejszych technologii, w tym Svelte i SvelteKit. Oferujemy:

  • Budowę aplikacji webowych od podstaw w Svelte/SvelteKit
  • Migrację istniejących projektów z React lub Vue do Svelte
  • Optymalizację wydajności istniejących aplikacji
  • Konsultacje technologiczne i dobór najlepszego stosu technologicznego
  • Wsparcie i utrzymanie aplikacji produkcyjnych

Skontaktuj się z nami, aby omówić Twój projekt i odkryć, jak Svelte może przyspieszyć rozwój Twojej aplikacji!

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

Svelte i SvelteKit - Nowa era tworzenia aplikacji frontendowych | MDS Software Solutions Group | MDS Software Solutions Group