Skip to main content
Svelte components can be rendered on the server, sent as HTML to the browser, and then made interactive through a process called hydration. This approach improves initial page load performance and SEO.

Rendering Modes

Client-Side Rendering (CSR)

The browser downloads JavaScript, executes it, and renders the page:
import { mount } from 'svelte';
import App from './App.svelte';

// Component is created and rendered in the browser
const app = mount(App, {
  target: document.body,
  props: { name: 'world' }
});
Pros:
  • Full interactivity immediately
  • Simpler deployment (static hosting)
  • No server required
Cons:
  • Slower initial load
  • Poor SEO (content not in initial HTML)
  • Blank page until JavaScript executes

Server-Side Rendering (SSR)

The server generates HTML and sends it to the browser:
import { render } from 'svelte/server';
import App from './App.svelte';

// Generate HTML on the server
const { html, head } = await render(App, {
  props: { name: 'world' }
});

const response = `
  <!DOCTYPE html>
  <html>
    <head>${head}</head>
    <body>${html}</body>
  </html>
`;
Pros:
  • Fast initial page load
  • Better SEO (crawlers see content)
  • Works without JavaScript
Cons:
  • No interactivity until JavaScript loads
  • Requires server infrastructure
  • More complex deployment

Hydration (SSR + CSR)

Combine SSR and CSR for the best of both worlds:
  1. Server renders component to HTML
  2. Browser receives and displays HTML immediately
  3. JavaScript downloads and executes
  4. Hydration attaches event listeners and makes the page interactive
import { hydrate } from 'svelte';
import App from './App.svelte';

// Pick up server-rendered HTML and make it interactive
const app = hydrate(App, {
  target: document.body,
  props: { name: 'world' }
});
Svelte components are always hydratable in Svelte 5. The hydratable compiler option from Svelte 4 has been removed.

Using the Render API

Basic Server Rendering

The render function from svelte/server generates HTML:
import { render } from 'svelte/server';
import App from './App.svelte';

const result = await render(App, {
  props: { user: { name: 'Alice' } }
});

console.log(result.html);  // <div>Hello Alice</div>
console.log(result.head);  // <svelte:head> contents

Handling <svelte:head>

Content in <svelte:head> is returned separately:
App.svelte
<svelte:head>
  <title>My App</title>
  <meta name="description" content="An amazing app" />
</svelte:head>

<h1>Welcome</h1>
const { html, head } = await render(App);
// head: '<title>My App</title><meta name="description"...'
// html: '<h1>Welcome</h1>'

Async Server Rendering

Svelte 5 supports asynchronous server rendering with await expressions:
<script>
  import { getUser } from './api';
  
  // This works on the server!
  const user = await getUser();
</script>

<h1>Hello {user.name}</h1>
import { render } from 'svelte/server';
import App from './App.svelte';

// await the render call
const { html } = await render(App);
When using await in components, you must await the render() call. The promise will resolve once all async operations complete.

Hydration Process

How Hydration Works

During hydration, Svelte:
  1. Walks the server-rendered DOM
  2. Attaches event listeners
  3. Initializes reactive state
  4. Makes the application interactive
// server.js - Generate HTML
const { html, head } = await render(App, { props: { count: 0 } });

// client.js - Hydrate the HTML
import { hydrate } from 'svelte';
hydrate(App, { target: document.body, props: { count: 0 } });
Props passed to hydrate() should match the props used during render() to avoid hydration mismatches.

Hydration Markers

Svelte 5 uses HTML comments as markers for efficient hydration:
<!-- Server-rendered HTML includes markers -->
<div>
  <!--[-->Hello<!--]-->
</div>
Don’t remove comments from server-rendered HTML! They’re essential for proper hydration.

Hydration Mismatches

A hydration mismatch occurs when server and client HTML differ:
<script>
  // ❌ Bad: Different value on server vs client
  const time = new Date().toLocaleTimeString();
</script>

<p>The time is {time}</p>
This causes a hydration_mismatch warning because the time will be different when rendered on the server vs. when hydrating on the client. Solutions:
  1. Use client-only rendering for dynamic values:
<script>
  import { browser } from '$app/environment';
  let time = $state('');
  
  $effect(() => {
    time = new Date().toLocaleTimeString();
  });
</script>

{#if browser}
  <p>The time is {time}</p>
{/if}
  1. Use hydratable for values that should be consistent:
<script>
  import { hydratable } from 'svelte';
  
  // Same value on server and client
  const time = await hydratable('time', () => 
    new Date().toLocaleTimeString()
  );
</script>

Optimizing Data Fetching

The hydratable API

Avoid fetching data twice (server + client) with hydratable:
<script>
  import { hydratable } from 'svelte';
  import { getUser } from './api';

  // ✅ Good: Fetched once on server, reused on client
  const user = await hydratable('user', () => getUser());
</script>

<h1>{user.name}</h1>
  1. Server: Runs getUser(), serializes result into HTML
  2. Client: Reads serialized data, skips getUser() call
  3. Post-hydration: Future calls run getUser() normally

Serialization

hydratable uses devalue which supports:
  • Primitives (string, number, boolean, null, undefined)
  • Objects and arrays
  • Date, Map, Set, RegExp
  • BigInt, URL
  • Promises (Svelte-specific enhancement)
<script>
  import { hydratable } from 'svelte';

  const data = await hydratable('data', () => ({
    date: new Date(),
    users: new Set(['alice', 'bob']),
    promise: Promise.resolve(42)
  }));
</script>

{await data.promise}
Functions, symbols, and DOM nodes cannot be serialized. Keep hydratable data JSON-like when possible.

Content Security Policy (CSP)

hydratable injects a script tag. For CSP compliance, provide a nonce:
import { render } from 'svelte/server';
import App from './App.svelte';

const nonce = crypto.randomUUID();

const { head, body } = await render(App, {
  csp: { nonce }
});

// Add the same nonce to your CSP header
response.headers.set(
  'Content-Security-Policy',
  `script-src 'nonce-${nonce}'`
);
For static sites, use hash-based CSP:
const { head, body, hashes } = await render(App, {
  csp: { hash: true }
});

const csp = `script-src ${hashes.script.map(h => `'${h}'`).join(' ')}`;
Prefer nonce over hash for dynamic SSR. Hash-based CSP will interfere with streaming SSR in future Svelte versions.

SSR Considerations

Browser-Only Code

Some code should only run in the browser:
<script>
  import { browser } from '$app/environment';
  
  // ❌ Bad: window is undefined on server
  const width = window.innerWidth;
  
  // ✅ Good: Check environment
  let width = $state(0);
  
  $effect(() => {
    // Effects don't run on server
    width = window.innerWidth;
  });
</script>
$effect callbacks never run on the server, so you don’t need if (browser) checks inside them.

Lifecycle Hooks

Only onDestroy runs during SSR:
<script>
  import { onMount, onDestroy } from 'svelte';
  
  onMount(() => {
    console.log('Client only');
  });
  
  onDestroy(() => {
    console.log('Runs on both server and client');
  });
</script>

Invalid HTML Structure

Browsers “repair” invalid HTML, causing hydration mismatches:
<!-- ❌ Bad: Browser will restructure this -->
<table>
  <tr><td>Cell</td></tr>
</table>

<!-- ✅ Good: Valid HTML structure -->
<table>
  <tbody>
    <tr><td>Cell</td></tr>
  </tbody>
</table>
Svelte warns about invalid structures with node_invalid_placement_ssr.

Using SvelteKit

For production applications, use SvelteKit which handles:
  • Automatic SSR/hydration setup
  • Data loading with load functions
  • Routing and navigation
  • Deployment adapters
  • Streaming SSR
  • Prerendering
+page.svelte
<script>
  // SvelteKit's load function runs on server
  export let data;
</script>

<h1>{data.title}</h1>
+page.server.js
export async function load() {
  return {
    title: 'My Page'
  };
}
SvelteKit is the recommended way to build Svelte apps with SSR. It handles the complexity of server/client coordination for you.

Best Practices

  1. Match server and client props - Prevents hydration mismatches
  2. Use hydratable for async data - Avoid duplicate fetching
  3. Respect browser-only APIs - Check for window, document availability
  4. Write valid HTML - Prevents browser repairs that break hydration
  5. Test with JavaScript disabled - Ensure core content is accessible
  6. Don’t remove HTML comments - Required for Svelte 5 hydration