teta.so

SvelteKit Server-Side Rendering (SSR): How It Works

Understand how SvelteKit handles SSR, including server load functions, streaming, hydration, prerendering, and performance optimization.

Server-side rendering is one of the core capabilities that separates SvelteKit from client-only Svelte applications. When a user requests a page, SvelteKit renders it to HTML on the server before sending it to the browser. This means the user sees content immediately — no blank page waiting for JavaScript to download, parse, and execute. SSR improves perceived performance, search engine indexing, and accessibility. SvelteKit makes SSR the default behavior and provides granular control over when and how it happens. This guide explains SvelteKit's SSR model in detail, from server load functions to streaming, hydration, and prerendering.

What Is Server-Side Rendering?

Server-side rendering means generating the HTML for a web page on the server in response to each request, rather than sending an empty shell and relying on client-side JavaScript to build the page.

Without SSR (a typical single-page application), the browser receives something like this:

<body>
  <div id="app"></div>
  <script src="/bundle.js"></script>
</body>

The user sees a blank page until bundle.js downloads, parses, executes, fetches data from an API, and renders the UI. On a fast connection, this might take a second. On a slow mobile connection, it can take several seconds — an eternity in web performance terms.

With SSR, the browser receives the fully rendered HTML:

<body>
  <div id="app">
    <h1>Welcome back, Alex</h1>
    <ul>
      <li>Task 1: Design homepage</li>
      <li>Task 2: Write API docs</li>
    </ul>
  </div>
  <script src="/bundle.js"></script>
</body>

The user sees content immediately. JavaScript then "hydrates" the page — attaching event listeners and making it interactive — without re-rendering the visible content.

SSR matters for three main reasons: performance (faster first contentful paint), SEO (search engines can index the fully rendered page), and accessibility (content is available even if JavaScript fails to load).

How SvelteKit Handles SSR

SvelteKit's SSR is built into its routing system. Every route is server-rendered by default. When a request comes in, SvelteKit:

  1. Matches the URL to a route
  2. Runs the route's server load function(s) to fetch data
  3. Renders the Svelte component tree to HTML
  4. Sends the HTML response with embedded data for hydration
  5. The browser displays the HTML immediately
  6. SvelteKit's client-side router takes over once JavaScript loads

This happens automatically. You do not need to configure anything to get SSR — it just works.

The Request Lifecycle

Here is what happens for a request to /app/dashboard:

Browser → GET /app/dashboard
  ↓
SvelteKit Server
  ↓ Run hooks.server.ts (middleware)
  ↓ Run +layout.server.ts load functions (parent → child)
  ↓ Run +page.server.ts load function
  ↓ Render +layout.svelte → +page.svelte to HTML
  ↓ Serialize load function data as JSON
  ↓ Return HTML + serialized data
  ↓
Browser
  ↓ Display HTML (user sees content)
  ↓ Download JavaScript
  ↓ Hydrate (attach interactivity)
  ↓ SvelteKit router takes over navigation

Subsequent navigations within the app use client-side routing — the browser fetches only the data (from the server load functions) and renders the new page client-side, without a full page reload.

Server Load Functions

Server load functions are the primary way to fetch data for SSR in SvelteKit. They run exclusively on the server and have access to databases, file systems, secrets, and other server-only resources.

Basic Server Load Function

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

export async function load() {
  const posts = await db.query('SELECT id, title, excerpt, published_at FROM posts ORDER BY published_at DESC');

  return { posts };
}

The data returned by load is available in the corresponding +page.svelte component:

<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
  let { data } = $props();
</script>

<h1>Blog</h1>
{#each data.posts as post}
  <article>
    <h2><a href="/blog/{post.id}">{post.title}</a></h2>
    <p>{post.excerpt}</p>
  </article>
{/each}

Dynamic Parameters

Route parameters are available in the params object:

// src/routes/blog/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/database';

export async function load({ params }) {
  const post = await db.query('SELECT * FROM posts WHERE slug = $1', [params.slug]);

  if (!post) {
    error(404, 'Post not found');
  }

  return { post };
}

Layout Load Functions

Layout load functions run for all child routes and are useful for shared data like navigation items or user profiles:

// src/routes/app/+layout.server.ts
export async function load({ locals }) {
  const { data: { session } } = await locals.supabase.auth.getSession();

  if (!session) {
    return { user: null };
  }

  const { data: profile } = await locals.supabase
    .from('profiles')
    .select('name, avatar_url')
    .eq('id', session.user.id)
    .single();

  return { user: profile };
}

The layout data is available to all child pages through their data prop, merged with the page's own data.

Universal Load Functions

Not all load functions need to run on the server. Universal load functions (in +page.ts or +layout.ts) run on the server during SSR and on the client during subsequent navigations:

// src/routes/weather/+page.ts
export async function load({ fetch }) {
  // This fetch is handled by SvelteKit during SSR
  // and by the browser during client-side navigation
  const response = await fetch('https://api.weather.com/current');
  const weather = await response.json();
  return { weather };
}

Use universal load functions when the data comes from a public API and does not require server-only secrets. Use server load functions when you need database access, environment variables, or other server-only resources.

Streaming with SvelteKit

SvelteKit supports streaming SSR, which allows you to send parts of the page to the browser before all data has loaded. This is particularly useful when some data is fast to fetch and other data is slow.

// src/routes/dashboard/+page.server.ts
export async function load() {
  // Fast query - included in initial HTML
  const stats = await db.getQuickStats();

  // Slow query - streamed when ready
  const detailedReport = db.getDetailedReport(); // Note: no await

  return {
    stats,
    streamed: {
      report: detailedReport
    }
  };
}

In the component, use {#await} to handle the streaming data:

<script lang="ts">
  let { data } = $props();
</script>

<!-- Renders immediately -->
<div class="stats">
  <p>Total users: {data.stats.users}</p>
  <p>Revenue: ${data.stats.revenue}</p>
</div>

<!-- Streams in when ready -->
{#await data.streamed.report}
  <p>Loading detailed report...</p>
{:then report}
  <table>
    {#each report.rows as row}
      <tr><td>{row.name}</td><td>{row.value}</td></tr>
    {/each}
  </table>
{/await}

The browser receives the stats immediately and shows a loading indicator for the report. When the slow query completes, the report streams in without a page reload.

CSR vs SSR vs SSG in SvelteKit

SvelteKit supports all three rendering strategies, and you can mix them within the same application.

SSR (Server-Side Rendering)

The default. Pages are rendered on the server for every request. Best for dynamic content that changes frequently or is personalized per user.

SSG (Static Site Generation / Prerendering)

Pages are rendered at build time and served as static HTML. Best for content that rarely changes — blog posts, documentation, landing pages.

Enable prerendering for a specific page:

// src/routes/about/+page.ts
export const prerender = true;

Or for all pages by default in svelte.config.js:

export default {
  kit: {
    prerender: {
      default: true
    }
  }
};

SvelteKit automatically crawls your app during build and prerenders all discoverable pages.

CSR (Client-Side Rendering)

Disable SSR for a page and render it entirely on the client. Useful for dashboards or admin panels that do not need SEO.

// src/routes/app/editor/+page.ts
export const ssr = false;

You can combine these strategies. A marketing site might prerender its landing pages (SSG), server-render its blog (SSR for freshness), and client-render its app dashboard (CSR for speed after login).

Hydration in SvelteKit

Hydration is the process of making server-rendered HTML interactive on the client. After the browser receives the HTML and displays it, SvelteKit's JavaScript loads and "hydrates" the page — attaching event listeners, setting up reactive state, and enabling client-side navigation.

Svelte's hydration is lighter than most frameworks. Because Svelte compiles components to direct DOM manipulation code (no virtual DOM), it does not need to reconstruct a component tree in memory and diff it against the HTML. Instead, it walks the existing DOM nodes, attaches event listeners, and connects reactive bindings.

Svelte 5's fine-grained reactivity via runes makes this even more efficient. Each $state and $derived value tracks exactly which DOM nodes depend on it, so hydration only sets up the specific connections needed.

In practice, SvelteKit's hydration is fast enough that users rarely notice it. The page appears fully rendered from the server-sent HTML, and interactivity becomes available within milliseconds of JavaScript loading.

Performance Tips for SvelteKit SSR

Avoid Waterfalls in Load Functions

If your page needs data from multiple sources, fetch them in parallel:

// Bad: sequential fetches (slow)
export async function load({ locals: { supabase } }) {
  const posts = await supabase.from('posts').select('*');
  const categories = await supabase.from('categories').select('*');
  return { posts: posts.data, categories: categories.data };
}

// Good: parallel fetches (fast)
export async function load({ locals: { supabase } }) {
  const [posts, categories] = await Promise.all([
    supabase.from('posts').select('*'),
    supabase.from('categories').select('*')
  ]);
  return { posts: posts.data, categories: categories.data };
}

Use Streaming for Slow Data

As shown above, stream non-critical data instead of blocking the entire page render.

Prerender Static Pages

Any page that does not change per request should be prerendered. This eliminates server load and delivers the page from a CDN edge node.

Minimize Data Size

Only return the fields you need from the server. Avoid sending entire database rows when the page only uses a few columns:

// Instead of select('*'), select only what you need
const { data } = await supabase
  .from('posts')
  .select('id, title, excerpt, published_at');

Tools like Teta generate SvelteKit code that follows these patterns by default, helping you build performant applications from the start.

FAQ

Does SvelteKit use SSR by default?

Yes, SvelteKit server-renders all pages by default. When a user visits a URL directly (not via client-side navigation), SvelteKit runs the page's load functions on the server, renders the components to HTML, and sends the complete page to the browser. You do not need to configure anything to enable SSR — it is the default behavior.

What is the difference between SSR and SSG in SvelteKit?

SSR renders pages on the server at request time, generating fresh HTML for every visit. SSG (prerendering) renders pages at build time, producing static HTML files that are served from a CDN. Use SSR for dynamic or personalized content and SSG for pages that rarely change. SvelteKit lets you mix both strategies in the same app by setting export const prerender = true on individual pages.

How do I prerender pages in SvelteKit?

Add export const prerender = true to a page's +page.ts or +page.server.ts file. SvelteKit will render that page at build time and include it as a static HTML file in the build output. For an entire app, set prerender: { default: true } in svelte.config.js. Dynamic pages with parameters will be prerendered for all discoverable URLs.

Can I disable SSR in SvelteKit?

Yes, add export const ssr = false to a page's +page.ts file. That page will be rendered entirely on the client — the server sends a minimal HTML shell, and JavaScript builds the page in the browser. This is useful for admin dashboards or app sections that do not need SEO. You can disable SSR globally by setting ssr: false in your SvelteKit config, though this is rarely recommended.

Frequently Asked Questions

Does SvelteKit use SSR by default?

Yes, SvelteKit server-renders all pages by default. When a user visits a URL directly (not via client-side navigation), SvelteKit runs the page's load functions on the server, renders the components to HTML, and sends the complete page to the browser. You do not need to configure anything to enable SSR — it is the default behavior.

What is the difference between SSR and SSG in SvelteKit?

SSR renders pages on the server at request time, generating fresh HTML for every visit. SSG (prerendering) renders pages at build time, producing static HTML files that are served from a CDN. Use SSR for dynamic or personalized content and SSG for pages that rarely change. SvelteKit lets you mix both strategies in the same app by setting export const prerender = true on individual pages.

How do I prerender pages in SvelteKit?

Add export const prerender = true to a page's +page.ts or +page.server.ts file. SvelteKit will render that page at build time and include it as a static HTML file in the build output. For an entire app, set prerender: { default: true } in svelte.config.js . Dynamic pages with parameters will be prerendered for all discoverable URLs.

Can I disable SSR in SvelteKit?

Yes, add export const ssr = false to a page's +page.ts file. That page will be rendered entirely on the client — the server sends a minimal HTML shell, and JavaScript builds the page in the browser. This is useful for admin dashboards or app sections that do not need SEO. You can disable SSR globally by setting ssr: false in your SvelteKit config, though this is rarely recommended.

Ready to start building?

Create your next web app with AI-powered development tools.

Inizia Gratis
← All articles