Server rendering and async UI have the trickiest edge cases: mismatches, leaked state and loading coordination.
Server rendering: render(App, { props }) from svelte/server returns { head, body } HTML. Only component scripts and markup run; $effect, onMount and attachments do not, so browser-only work belongs there. On the client, `hydrate(App, { target, props })` adopts the existing DOM instead of recreating it. SvelteKit does both for you.
Hydration expects the client's first render to produce the same output. Non-deterministic values (random ids, Date.now(), window checks during render) cause mismatches; use $props.id() for ids and compute browser-only values in effects. Remember that module-level state is shared across all server requests.
`<svelte:boundary>` (5.3+) catches errors thrown while rendering or in effects of its children: provide a failed snippet, which receives (error, reset), and an onerror callback for logging. This keeps one broken widget from blanking the page.
Experimental async (5.36+): with the experimental.async compiler option you can await directly in <script>, in $derived expressions and in markup. A boundary's pending snippet shows while its contents first load; later updates keep the old UI until new data is ready, and $effect.pending() reports in-flight work. Independent awaits in the same template run in parallel, and updates are coordinated so the UI never shows a mix of old and new data. Because the API can still change, check the docs before using it in production.
Example
<!-- svelte.config.js: export default { compilerOptions: { experimental: { async: true } } } -->
<script>
import Chart from './Chart.svelte';
let id = $state(1);
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
</script>
<button onclick={() => id++}>next</button>
{#if $effect.pending()}<span>updating...</span>{/if}
<svelte:boundary onerror={(e) => console.error(e)}>
<h2>{(await getUser(id)).name}</h2> <!-- await in markup (experimental) -->
<Chart userId={id} />
{#snippet pending()}<p>Loading...</p>{/snippet}
{#snippet failed(error, reset)}
<p>Error: {error.message}</p><button onclick={reset}>retry</button>
{/snippet}
</svelte:boundary>Remember
render(server) andhydrate(client); effects never run on the server- Avoid non-deterministic render output; use
$props.id()for ids <svelte:boundary>withfailedandpendingsnippets isolates errors and loadingawaitin markup and deriveds is experimental behindexperimental.async
Watch out for
- Reading
windoworlocalStorageat the top level of a component script, which crashes during SSR.