Skip to main content
← All Articles Β· Β· 14 min read

SvelteKit 2 Production Guide 2026: Runes, Server Actions & Performance

Complete SvelteKit 2 production guide for 2026: Svelte 5 Runes reactivity model, server actions, universal rendering, Cloudflare/Vercel deployment, and performance optimization techniques.

sveltekitsveltejavascriptweb-frameworkperformance

SvelteKit 2 Production Guide 2026: Runes, Server Actions & Performance

SvelteKit 2 with Svelte 5 Runes has solidified Svelte’s position as the most ergonomic full-stack web framework in 2026. The Runes system replaced the implicit reactivity model with explicit, fine-grained reactivity primitivesβ€”making Svelte more predictable at scale while preserving its legendary developer experience.

This guide covers SvelteKit 2 in production: Runes reactivity, server actions, streaming, deployment, and performance patterns.

Why SvelteKit in 2026?

The numbers speak:

  • No virtual DOM: Svelte compiles to direct DOM manipulation, skipping VDOM overhead
  • Bundle sizes: ~10–30KB for a typical SvelteKit app vs 100KB+ for React apps
  • Lighthouse scores: 95–100 out of the box vs significant optimization required for React
  • Developer satisfaction: Consistently top-rated in State of JS surveys

SvelteKit 2 + Svelte 5 delivers:

  • Runes: $state, $derived, $effect, $props β€” explicit reactivity primitives
  • Server actions: Form-based mutations with progressive enhancement
  • Universal rendering: SSR, CSR, SSG, and streaming in one framework
  • Adapter ecosystem: Deploy anywhere β€” Vercel, Cloudflare, Node, Deno, static

Project Setup

# Create a new SvelteKit 2 project
npm create svelte@latest my-app
cd my-app

# Choose: SvelteKit demo app, TypeScript, ESLint, Prettier

npm install
npm run dev
my-app/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ lib/              # Shared utilities ($lib alias)
β”‚   β”œβ”€β”€ routes/           # File-based routing
β”‚   β”‚   β”œβ”€β”€ +page.svelte  # Page component
β”‚   β”‚   β”œβ”€β”€ +page.ts      # Universal load function
β”‚   β”‚   β”œβ”€β”€ +page.server.ts # Server-only load + actions
β”‚   β”‚   └── +layout.svelte # Layout component
β”‚   β”œβ”€β”€ app.html          # HTML shell
β”‚   └── app.d.ts          # Type declarations
β”œβ”€β”€ static/               # Static assets
β”œβ”€β”€ svelte.config.js      # SvelteKit config
└── vite.config.ts        # Vite config

Svelte 5 Runes Deep Dive

Runes are the core innovation of Svelte 5. They replace Svelte 4’s implicit reactive declarations ($:) with explicit primitives that work everywhereβ€”components, .svelte.ts files, and shared modules.

$state β€” Reactive State

<!-- Counter.svelte -->
<script>
  // $state creates reactive state
  let count = $state(0);

  // Objects and arrays are deeply reactive
  let user = $state({
    name: "Alice",
    preferences: {
      theme: "dark",
      notifications: true
    }
  });

  function increment() {
    count++;
  }

  function updateTheme(theme) {
    // Nested mutation triggers reactivity
    user.preferences.theme = theme;
  }
</script>

<button onclick={increment}>
  Count: {count}
</button>

<p>Theme: {user.preferences.theme}</p>

$derived β€” Computed Values

<script>
  let items = $state([
    { id: 1, name: "Apple", price: 1.5, inCart: false },
    { id: 2, name: "Banana", price: 0.5, inCart: true },
    { id: 3, name: "Cherry", price: 3.0, inCart: true },
  ]);

  // $derived recalculates when dependencies change
  let cartItems = $derived(items.filter(item => item.inCart));
  let cartTotal = $derived(
    cartItems.reduce((sum, item) => sum + item.price, 0)
  );
  let formattedTotal = $derived(`$${cartTotal.toFixed(2)}`);

  function toggleCart(id) {
    const item = items.find(i => i.id === id);
    if (item) item.inCart = !item.inCart;
  }
</script>

<p>Cart: {cartItems.length} items β€” {formattedTotal}</p>

$effect β€” Side Effects

<script>
  let searchQuery = $state("");
  let results = $state([]);
  let loading = $state(false);

  // $effect runs when dependencies change
  $effect(() => {
    if (searchQuery.length < 2) {
      results = [];
      return;
    }

    loading = true;

    // Cleanup function β€” runs before next effect or unmount
    const controller = new AbortController();

    fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`, {
      signal: controller.signal
    })
      .then(r => r.json())
      .then(data => {
        results = data.items;
        loading = false;
      })
      .catch(err => {
        if (err.name !== "AbortError") {
          console.error(err);
          loading = false;
        }
      });

    return () => {
      controller.abort(); // Cleanup on re-run
    };
  });
</script>

$props β€” Component Props

<!-- UserCard.svelte -->
<script>
  // Typed props with defaults
  let {
    name,
    email,
    role = "member",
    onEdit = () => {},
  } = $props();
</script>

<div class="card">
  <h3>{name}</h3>
  <p>{email}</p>
  <span class="badge">{role}</span>
  <button onclick={onEdit}>Edit</button>
</div>
<!-- Parent usage -->
<UserCard
  name="Alice"
  email="[email protected]"
  role="admin"
  onEdit={() => openEditModal(user)}
/>

Shared Reactive State (Runes Stores)

// src/lib/cart.svelte.ts
export function createCart() {
  let items = $state<CartItem[]>([]);

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

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

  function add(product: Product) {
    const existing = items.find(i => i.id === product.id);
    if (existing) {
      existing.quantity++;
    } else {
      items.push({ ...product, quantity: 1 });
    }
  }

  function remove(id: string) {
    const index = items.findIndex(i => i.id === id);
    if (index >= 0) items.splice(index, 1);
  }

  return { items, total, count, add, remove };
}

export const cart = createCart();
<!-- Any component can use the shared cart -->
<script>
  import { cart } from "$lib/cart.svelte.ts";
</script>

<p>Cart: {cart.count} items β€” ${cart.total.toFixed(2)}</p>
<button onclick={() => cart.add(product)}>Add to Cart</button>

Data Loading

Universal Load Functions

// src/routes/products/+page.ts
// Runs on both server and client
import type { PageLoad } from "./$types";

export const load: PageLoad = async ({ fetch, params, url }) => {
  const category = url.searchParams.get("category") ?? "all";
  const page = Number(url.searchParams.get("page") ?? "1");

  const response = await fetch(`/api/products?category=${category}&page=${page}`);

  if (!response.ok) {
    throw new Error("Failed to load products");
  }

  const data = await response.json();

  return {
    products: data.items,
    total: data.total,
    page,
    category,
  };
};

Server-Only Load (Access DB Directly)

// src/routes/dashboard/+page.server.ts
// Only runs on server β€” can access DB, secrets, etc.
import type { PageServerLoad } from "./$types";
import { db } from "$lib/server/db";
import { redirect } from "@sveltejs/kit";

export const load: PageServerLoad = async ({ locals }) => {
  // locals.user set by auth hook
  if (!locals.user) {
    redirect(302, "/login");
  }

  const [orders, metrics] = await Promise.all([
    db.orders.findMany({ where: { userId: locals.user.id }, take: 10 }),
    db.metrics.getForUser(locals.user.id),
  ]);

  return {
    user: locals.user,
    orders,
    metrics,
  };
};

Server Actions

Server actions are the SvelteKit way to handle form mutations. They run on the server, support progressive enhancement, and handle validation elegantly.

// src/routes/products/[id]/+page.server.ts
import { fail, redirect } from "@sveltejs/kit";
import { z } from "zod";
import { db } from "$lib/server/db";
import type { Actions, PageServerLoad } from "./$types";

const reviewSchema = z.object({
  rating: z.coerce.number().int().min(1).max(5),
  comment: z.string().min(10).max(500),
});

export const load: PageServerLoad = async ({ params }) => {
  const product = await db.products.findUnique({
    where: { id: params.id },
    include: { reviews: { orderBy: { createdAt: "desc" }, take: 10 } },
  });

  if (!product) throw new Response("Not found", { status: 404 });

  return { product };
};

export const actions: Actions = {
  // POST /products/[id]?/review
  review: async ({ request, locals, params }) => {
    if (!locals.user) {
      return fail(401, { error: "Must be logged in" });
    }

    const formData = await request.formData();
    const raw = {
      rating: formData.get("rating"),
      comment: formData.get("comment"),
    };

    const result = reviewSchema.safeParse(raw);
    if (!result.success) {
      return fail(400, {
        errors: result.error.flatten().fieldErrors,
        values: raw,
      });
    }

    await db.reviews.create({
      data: {
        productId: params.id,
        userId: locals.user.id,
        ...result.data,
      },
    });

    return { success: true };
  },
};
<!-- src/routes/products/[id]/+page.svelte -->
<script>
  import { enhance } from "$app/forms";
  let { data, form } = $props();
</script>

<h1>{data.product.name}</h1>

<!-- Progressive enhancement: works without JavaScript too -->
<form method="POST" action="?/review" use:enhance>
  <div>
    <label for="rating">Rating</label>
    <select name="rating" id="rating" value={form?.values?.rating ?? ""}>
      {#each [1, 2, 3, 4, 5] as rating}
        <option value={rating}>{rating} stars</option>
      {/each}
    </select>
    {#if form?.errors?.rating}
      <p class="error">{form.errors.rating[0]}</p>
    {/if}
  </div>

  <div>
    <label for="comment">Comment</label>
    <textarea name="comment" id="comment">{form?.values?.comment ?? ""}</textarea>
    {#if form?.errors?.comment}
      <p class="error">{form.errors.comment[0]}</p>
    {/if}
  </div>

  <button type="submit">Submit Review</button>
</form>

{#if form?.success}
  <p class="success">Review submitted!</p>
{/if}

Streaming and Deferred Data

SvelteKit supports streaming responses, which lets you send the page shell immediately and stream in slow data.

// src/routes/dashboard/+page.server.ts
export const load: PageServerLoad = async () => {
  // Fast: send immediately
  const user = await db.users.findFirst();

  // Slow: don't await, stream it in
  const slowAnalytics = db.analytics.getHeavyReport(); // Promise, not awaited

  return {
    user, // Available immediately in the page
    streamed: {
      analytics: slowAnalytics, // Streams in when ready
    },
  };
};
<script>
  let { data } = $props();
</script>

<!-- Renders immediately -->
<h1>Welcome, {data.user.name}</h1>

<!-- Suspense-like streaming -->
{#await data.streamed.analytics}
  <p>Loading analytics...</p>
{:then analytics}
  <AnalyticsDashboard {analytics} />
{:catch error}
  <p>Failed to load analytics</p>
{/await}

Performance Optimization

Code Splitting

SvelteKit automatically code-splits at route boundaries. For large components, use dynamic imports:

<script>
  import { onMount } from "svelte";

  let ChartComponent = $state(null);

  onMount(async () => {
    // Dynamic import β€” only loads when mounted
    const module = await import("$lib/Chart.svelte");
    ChartComponent = module.default;
  });
</script>

{#if ChartComponent}
  <svelte:component this={ChartComponent} {data} />
{:else}
  <div class="chart-skeleton" />
{/if}

Image Optimization

<!-- Use srcset for responsive images -->
<img
  src="/images/hero.webp"
  srcset="/images/hero-400.webp 400w, /images/hero-800.webp 800w, /images/hero-1200.webp 1200w"
  sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"
  alt="Hero image"
  loading="lazy"
  decoding="async"
/>

Preloading

<!-- Preload data on hover for instant navigation -->
<a href="/products" data-sveltekit-preload-data="hover">
  Browse Products
</a>

<!-- Eager preload: fetch immediately on page load -->
<a href="/dashboard" data-sveltekit-preload-data="eager">
  Dashboard
</a>

Deployment

Vercel Deployment

npm i -D @sveltejs/adapter-vercel
// svelte.config.js
import adapter from "@sveltejs/adapter-vercel";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

export default {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter({
      runtime: "edge", // or "nodejs22.x"
      regions: ["iad1", "sfo1"],
    }),
  },
};

Cloudflare Pages

npm i -D @sveltejs/adapter-cloudflare
// svelte.config.js
import adapter from "@sveltejs/adapter-cloudflare";

export default {
  kit: {
    adapter: adapter({
      routes: {
        include: ["/*"],
        exclude: ["<all>"],
      },
    }),
  },
};

Node.js (Docker)

// svelte.config.js
import adapter from "@sveltejs/adapter-node";

export default {
  kit: {
    adapter: adapter({ out: "build" }),
  },
};
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/build ./build
COPY --from=builder /app/package*.json ./
RUN npm ci --production
ENV NODE_ENV=production PORT=3000
EXPOSE 3000
CMD ["node", "build"]

SvelteKit vs Next.js vs Nuxt in 2026

FeatureSvelteKit 2Next.js 15Nuxt 3
Bundle size~15KB~90KB~60KB
TypeScriptExcellentExcellentExcellent
Server actionsβœ… Nativeβœ… React Server Actionsβœ… Nuxt Actions
Reactivity modelRunes (compile-time)React Hooks (runtime)Vue Composition API
Learning curveLowMediumMedium
EcosystemGrowingMassiveLarge
Edge deploymentβœ… All adaptersβœ… Vercel Edgeβœ… Multiple
Performance (TTI)FastestGoodGood

Conclusion

SvelteKit 2 with Svelte 5 Runes is the most ergonomic full-stack framework available in 2026. The Runes system resolves the predictability issues of Svelte 4’s implicit reactivity while keeping the compile-time efficiency that makes Svelte apps so fast.

For new projects where you want excellent performance, small bundle sizes, and a delightful developer experience, SvelteKit 2 is an excellent choice. The server actions system and universal rendering make it competitive with Next.js for full-stack applications.


Related: React 19 New Features, Next.js App Router Guide, Web Performance 2026

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.