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:
- Server renders component to HTML
- Browser receives and displays HTML immediately
- JavaScript downloads and executes
- 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:
<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:
- Walks the server-rendered DOM
- Attaches event listeners
- Initializes reactive state
- 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:
- 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}
- 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>
- Server: Runs
getUser(), serializes result into HTML
- Client: Reads serialized data, skips
getUser() call
- 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
<script>
// SvelteKit's load function runs on server
export let data;
</script>
<h1>{data.title}</h1>
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
- Match server and client props - Prevents hydration mismatches
- Use
hydratable for async data - Avoid duplicate fetching
- Respect browser-only APIs - Check for
window, document availability
- Write valid HTML - Prevents browser repairs that break hydration
- Test with JavaScript disabled - Ensure core content is accessible
- Don’t remove HTML comments - Required for Svelte 5 hydration